feat: refactor

This commit is contained in:
Stein Milder 2022-12-16 13:10:57 +01:00
parent f55c43653c
commit 4df2adb984
27 changed files with 553 additions and 588 deletions

View file

@ -4,29 +4,5 @@
library flutter_community_chat; library flutter_community_chat;
import 'package:flutter/material.dart';
import 'package:flutter_community_chat/service/chat_service.dart';
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
export 'package:flutter_community_chat_view/flutter_community_chat_view.dart'; export 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
export 'package:flutter_community_chat/service/chat_service.dart';
export 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart'; export 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
class CommunityChat extends StatelessWidget {
const CommunityChat({
required this.chatService,
super.key,
});
final ChatService chatService;
@override
Widget build(BuildContext context) => ChatScreen(
chats: chatService.dataProvider.getChatsStream(),
onPressStartChat: () => chatService.onPressStartChat(context),
onPressChat: (chat) => chatService.onPressChat(context, chat),
onDeleteChat: (ChatModel chat) => chatService.deleteChat(chat),
options: chatService.options,
translations: chatService.translations(context),
);
}

View file

@ -1,130 +0,0 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_community_chat/ui/components/image_loading_snackbar.dart';
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
import 'package:flutter_image_picker/flutter_image_picker.dart';
abstract class ChatService {
ChatService({
required this.options,
required this.imagePickerConfig,
required this.dataProvider,
});
final CommunityChatInterface dataProvider;
final ChatOptions options;
final ImagePickerConfig imagePickerConfig;
bool _isFetchingUsers = false;
ImagePickerTheme imagePickerTheme(BuildContext context) =>
const ImagePickerTheme();
ChatTranslations translations(BuildContext context);
Future<void> _push(BuildContext context, Widget widget) =>
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => widget),
);
void _pop(BuildContext context) => Navigator.of(context).pop();
Future<void> onPressStartChat(BuildContext context) async {
if (!_isFetchingUsers) {
_isFetchingUsers = true;
await dataProvider.getChatUsers().then(
(users) {
_isFetchingUsers = false;
_push(
context,
buildNewChatScreen(context, users),
);
},
);
}
}
Widget buildNewChatScreen(
BuildContext context,
List<ChatUserModel> users,
) =>
NewChatScreen(
options: options,
translations: translations(context),
onPressCreateChat: (user) => onPressChat(
context,
PersonalChatModel(user: user),
popBeforePush: true,
),
users: users,
);
Future<void> onPressChat(
BuildContext context,
ChatModel chat, {
bool popBeforePush = false,
}) =>
dataProvider.setChat(chat).then((_) {
if (popBeforePush) {
_pop(context);
}
_push(
context,
buildChatDetailScreen(context, chat),
);
});
Widget buildChatDetailScreen(
BuildContext context,
ChatModel chat,
) =>
ChatDetailScreen(
options: options,
translations: translations(context),
chat: chat,
chatMessages: dataProvider.getMessagesStream(),
onPressSelectImage: (ChatModel chat) => onPressSelectImage(
context,
chat,
),
onMessageSubmit: (ChatModel chat, String content) =>
dataProvider.sendTextMessage(content),
);
Future<void> onPressSelectImage(
BuildContext context,
ChatModel chat,
) =>
showModalBottomSheet<Uint8List?>(
context: context,
builder: (BuildContext context) => options.imagePickerContainerBuilder(
ImagePicker(
customButton: options.closeImagePickerButtonBuilder(
context,
() => Navigator.of(context).pop(),
translations(context),
),
imagePickerConfig: imagePickerConfig,
imagePickerTheme: imagePickerTheme(context),
),
),
).then(
(image) async {
var messenger = ScaffoldMessenger.of(context);
messenger.showSnackBar(
getImageLoadingSnackbar(
translations(context),
),
);
if (image != null) {
await dataProvider.sendImageMessage(image);
}
messenger.hideCurrentSnackBar();
},
);
Future<void> deleteChat(ChatModel chat) => dataProvider.deleteChat(chat);
}

View file

@ -179,7 +179,7 @@ packages:
description: description:
path: "packages/flutter_community_chat_interface" path: "packages/flutter_community_chat_interface"
ref: HEAD ref: HEAD
resolved-ref: "6a9e88ec8d07118e1543b1a82aa5482d6832cbf8" resolved-ref: c644e1affb1dd4f570bf0e4ae2e950f5e9d83c37
url: "https://github.com/Iconica-Development/flutter_community_chat.git" url: "https://github.com/Iconica-Development/flutter_community_chat.git"
source: git source: git
version: "0.0.1" version: "0.0.1"
@ -188,7 +188,7 @@ packages:
description: description:
path: "packages/flutter_community_chat_view" path: "packages/flutter_community_chat_view"
ref: HEAD ref: HEAD
resolved-ref: "6a9e88ec8d07118e1543b1a82aa5482d6832cbf8" resolved-ref: c644e1affb1dd4f570bf0e4ae2e950f5e9d83c37
url: "https://github.com/Iconica-Development/flutter_community_chat.git" url: "https://github.com/Iconica-Development/flutter_community_chat.git"
source: git source: git
version: "0.0.1" version: "0.0.1"
@ -202,7 +202,7 @@ packages:
source: git source: git
version: "1.0.0" version: "1.0.0"
flutter_image_picker: flutter_image_picker:
dependency: "direct main" dependency: transitive
description: description:
path: "." path: "."
ref: "1.0.3" ref: "1.0.3"
@ -319,7 +319,7 @@ packages:
name: matcher name: matcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.13" version: "0.12.14"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
@ -361,7 +361,7 @@ packages:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.2" version: "1.8.3"
path_provider: path_provider:
dependency: transitive dependency: transitive
description: description:
@ -478,7 +478,7 @@ packages:
name: sqflite name: sqflite
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.0+3" version: "2.2.2"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:
@ -527,7 +527,7 @@ packages:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.16" version: "0.4.17"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:

View file

@ -19,11 +19,7 @@ dependencies:
git: git:
url: https://github.com/Iconica-Development/flutter_community_chat.git url: https://github.com/Iconica-Development/flutter_community_chat.git
path: packages/flutter_community_chat_interface path: packages/flutter_community_chat_interface
flutter_image_picker:
git:
url: https://github.com/Iconica-Development/flutter_image_picker
ref: 1.0.3
dev_dependencies: dev_dependencies:
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0

View file

@ -7,22 +7,9 @@ class FirebaseChatOptions {
this.chatsCollectionName = 'chats', this.chatsCollectionName = 'chats',
this.messagesCollectionName = 'messages', this.messagesCollectionName = 'messages',
this.usersCollectionName = 'users', this.usersCollectionName = 'users',
this.userFilter,
}); });
final String chatsCollectionName; final String chatsCollectionName;
final String messagesCollectionName; final String messagesCollectionName;
final String usersCollectionName; final String usersCollectionName;
final FirebaseUserFilter? userFilter;
}
class FirebaseUserFilter {
const FirebaseUserFilter({
required this.field,
required this.expectedValue,
});
final String field;
final Object expectedValue;
} }

View file

@ -4,84 +4,4 @@
library flutter_community_chat_firebase; library flutter_community_chat_firebase;
import 'dart:async';
import 'dart:typed_data';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter_community_chat_firebase/config/firebase_chat_options.dart';
import 'package:flutter_community_chat_firebase/service/service.dart';
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
export 'package:flutter_community_chat_firebase/service/service.dart'; export 'package:flutter_community_chat_firebase/service/service.dart';
class FirebaseCommunityChatDataProvider extends CommunityChatInterface {
late final FirebaseUserService _userService;
late final FirebaseMessageService _messageService;
late final FirebaseChatService _chatService;
final FirebaseChatOptions firebaseChatOptions;
FirebaseCommunityChatDataProvider({
this.firebaseChatOptions = const FirebaseChatOptions(),
FirebaseApp? app,
FirebaseUserService? firebaseUserService,
FirebaseMessageService? firebaseMessageService,
FirebaseChatService? firebaseChatService,
}) {
var appInstance = app ?? Firebase.app();
var db = FirebaseFirestore.instanceFor(app: appInstance);
var storage = FirebaseStorage.instanceFor(app: appInstance);
var auth = FirebaseAuth.instanceFor(app: appInstance);
_userService = firebaseUserService ??
FirebaseUserService(
db: db,
auth: auth,
options: firebaseChatOptions,
);
_chatService = firebaseChatService ??
FirebaseChatService(
db: db,
storage: storage,
userService: _userService,
options: firebaseChatOptions,
);
_messageService = firebaseMessageService ??
FirebaseMessageService(
db: db,
storage: storage,
userService: _userService,
chatService: _chatService,
options: firebaseChatOptions,
);
}
@override
Stream<List<ChatMessageModel>> getMessagesStream() =>
_messageService.getMessagesStream();
@override
Future<List<ChatUserModel>> getChatUsers() => _userService.getAllUsers();
@override
Stream<List<ChatModel>> getChatsStream() => _chatService.getChatsStream();
@override
Future<void> sendTextMessage(String text) =>
_messageService.sendTextMessage(text);
@override
Future<void> sendImageMessage(Uint8List image) =>
_messageService.sendImageMessage(image);
@override
Future<void> setChat(ChatModel chat) async =>
await _messageService.setChat(chat);
@override
Future<void> deleteChat(ChatModel chat) async =>
await _chatService.deleteChat(chat);
}

View file

@ -4,31 +4,38 @@
import 'dart:async'; import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_storage/firebase_storage.dart'; import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';
import 'package:flutter_community_chat_firebase/config/firebase_chat_options.dart'; import 'package:flutter_community_chat_firebase/config/firebase_chat_options.dart';
import 'package:flutter_community_chat_firebase/dto/firebase_chat_document.dart'; import 'package:flutter_community_chat_firebase/dto/firebase_chat_document.dart';
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart'; import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
import 'firebase_user_service.dart';
class FirebaseChatService { class FirebaseChatService implements ChatService {
late FirebaseFirestore _db;
late FirebaseStorage _storage;
late ChatUserService _userService;
late FirebaseChatOptions _options;
FirebaseChatService({ FirebaseChatService({
required this.db, required ChatUserService userService,
required this.storage, FirebaseApp? app,
required this.userService, FirebaseChatOptions? options,
required this.options, }) {
}); var appInstance = app ?? Firebase.app();
FirebaseFirestore db; _db = FirebaseFirestore.instanceFor(app: appInstance);
FirebaseStorage storage; _storage = FirebaseStorage.instanceFor(app: appInstance);
FirebaseUserService userService; _userService = userService;
FirebaseChatOptions options; _options = options ?? const FirebaseChatOptions();
}
StreamSubscription<QuerySnapshot> _addChatSubscription( StreamSubscription<QuerySnapshot> _addChatSubscription(
List<String> chatIds, List<String> chatIds,
Function(List<ChatModel>) onReceivedChats, Function(List<PersonalChatModel>) onReceivedChats,
) { ) {
var snapshots = db var snapshots = _db
.collection(options.chatsCollectionName) .collection(_options.chatsCollectionName)
.where( .where(
FieldPath.documentId, FieldPath.documentId,
whereIn: chatIds, whereIn: chatIds,
@ -41,8 +48,8 @@ class FirebaseChatService {
.snapshots(); .snapshots();
return snapshots.listen((snapshot) async { return snapshots.listen((snapshot) async {
var currentUser = await userService.getCurrentUser(); var currentUser = await _userService.getCurrentUser();
List<ChatModel> chats = []; List<PersonalChatModel> chats = [];
for (var chatDoc in snapshot.docs) { for (var chatDoc in snapshot.docs) {
var chatData = chatDoc.data(); var chatData = chatDoc.data();
@ -51,7 +58,7 @@ class FirebaseChatService {
if (chatData.lastMessage != null) { if (chatData.lastMessage != null) {
var messageData = chatData.lastMessage!; var messageData = chatData.lastMessage!;
var sender = await userService.getUser(messageData.sender); var sender = await _userService.getUser(messageData.sender);
if (sender != null) { if (sender != null) {
var timestamp = DateTime.fromMillisecondsSinceEpoch( var timestamp = DateTime.fromMillisecondsSinceEpoch(
@ -77,7 +84,7 @@ class FirebaseChatService {
var otherUserId = List<String>.from(chatData.users).firstWhere( var otherUserId = List<String>.from(chatData.users).firstWhere(
(element) => element != currentUser?.id, (element) => element != currentUser?.id,
); );
var otherUser = await userService.getUser(otherUserId); var otherUser = await _userService.getUser(otherUserId);
if (otherUser != null) { if (otherUser != null) {
chats.add( chats.add(
@ -117,14 +124,15 @@ class FirebaseChatService {
return result; return result;
} }
Stream<List<ChatModel>> _getSpecificChatsStream(List<String> chatIds) { Stream<List<PersonalChatModel>> _getSpecificChatsStream(
late StreamController<List<ChatModel>> controller; List<String> chatIds) {
late StreamController<List<PersonalChatModel>> controller;
List<StreamSubscription<QuerySnapshot>> subscriptions = []; List<StreamSubscription<QuerySnapshot>> subscriptions = [];
var splittedChatIds = _splitChatIds(chatIds: chatIds); var splittedChatIds = _splitChatIds(chatIds: chatIds);
controller = StreamController<List<ChatModel>>( controller = StreamController<List<PersonalChatModel>>(
onListen: () { onListen: () {
var chats = <int, List<ChatModel>>{}; var chats = <int, List<PersonalChatModel>>{};
for (var chatIdPair in splittedChatIds.asMap().entries) { for (var chatIdPair in splittedChatIds.asMap().entries) {
subscriptions.add( subscriptions.add(
@ -133,7 +141,7 @@ class FirebaseChatService {
(data) { (data) {
chats[chatIdPair.key] = data; chats[chatIdPair.key] = data;
List<ChatModel> mergedChats = []; List<PersonalChatModel> mergedChats = [];
mergedChats.addAll( mergedChats.addAll(
chats.values.expand((element) => element), chats.values.expand((element) => element),
@ -160,15 +168,17 @@ class FirebaseChatService {
return controller.stream; return controller.stream;
} }
Stream<List<ChatModel>> getChatsStream() { @override
late StreamController<List<ChatModel>> controller; Stream<List<PersonalChatModel>> getChatsStream() {
late StreamController<List<PersonalChatModel>> controller;
StreamSubscription? userChatsSubscription; StreamSubscription? userChatsSubscription;
StreamSubscription? chatsSubscription; StreamSubscription? chatsSubscription;
controller = StreamController( controller = StreamController(
onListen: () async { onListen: () async {
var currentUser = await userService.getCurrentUser(); debugPrint('Start listening to chats');
userChatsSubscription = db var currentUser = await _userService.getCurrentUser();
.collection(options.usersCollectionName) userChatsSubscription = _db
.collection(_options.usersCollectionName)
.doc(currentUser?.id) .doc(currentUser?.id)
.collection('chats') .collection('chats')
.snapshots() .snapshots()
@ -185,37 +195,34 @@ class FirebaseChatService {
onCancel: () { onCancel: () {
chatsSubscription?.cancel(); chatsSubscription?.cancel();
userChatsSubscription?.cancel(); userChatsSubscription?.cancel();
debugPrint('Stop listening to chats');
}, },
); );
return controller.stream; return controller.stream;
} }
Future<ChatModel?> getChatByUser(ChatUserModel user) async { @override
var currentUser = await userService.getCurrentUser(); Future<PersonalChatModel> getOrCreateChatByUser(ChatUserModel user) async {
var chatCollection = await db var currentUser = await _userService.getCurrentUser();
.collection(options.usersCollectionName) var collection = await _db
.collection(_options.usersCollectionName)
.doc(currentUser?.id) .doc(currentUser?.id)
.collection('chats') .collection('chats')
.where('users', arrayContains: user.id)
.get(); .get();
for (var element in chatCollection.docs) { var doc = collection.docs.isNotEmpty ? collection.docs.first : null;
var data = element.data();
if (data.containsKey('users') && data['users'] is List) {
if (data['users'].contains(user.id)) {
return PersonalChatModel(
id: element.id,
user: user,
);
}
}
}
return null; return PersonalChatModel(
id: doc?.id,
user: user,
);
} }
@override
Future<void> deleteChat(ChatModel chat) async { Future<void> deleteChat(ChatModel chat) async {
var chatCollection = await db var chatCollection = await _db
.collection(options.chatsCollectionName) .collection(_options.chatsCollectionName)
.doc(chat.id) .doc(chat.id)
.withConverter( .withConverter(
fromFirestore: (snapshot, _) => fromFirestore: (snapshot, _) =>
@ -228,8 +235,8 @@ class FirebaseChatService {
if (chatData != null) { if (chatData != null) {
for (var userId in chatData.users) { for (var userId in chatData.users) {
db _db
.collection(options.usersCollectionName) .collection(_options.usersCollectionName)
.doc(userId) .doc(userId)
.collection('chats') .collection('chats')
.doc(chat.id) .doc(chat.id)
@ -237,9 +244,12 @@ class FirebaseChatService {
} }
if (chat.id != null) { if (chat.id != null) {
await db.collection(options.chatsCollectionName).doc(chat.id).delete(); await _db
await storage .collection(_options.chatsCollectionName)
.ref(options.chatsCollectionName) .doc(chat.id)
.delete();
await _storage
.ref(_options.chatsCollectionName)
.child(chat.id!) .child(chat.id!)
.listAll() .listAll()
.then((value) { .then((value) {
@ -250,4 +260,48 @@ class FirebaseChatService {
} }
} }
} }
@override
Future<PersonalChatModel> storeChatIfNot(PersonalChatModel chat) async {
if (chat.id == null) {
var currentUser = await _userService.getCurrentUser();
if (currentUser?.id == null || chat.user.id == null) {
return chat;
}
List<String> userIds = [
currentUser!.id!,
chat.user.id!,
];
var reference = await _db
.collection(_options.chatsCollectionName)
.withConverter(
fromFirestore: (snapshot, _) =>
FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id),
toFirestore: (chat, _) => chat.toJson(),
)
.add(
FirebaseChatDocument(
personal: true,
users: userIds,
lastUsed: Timestamp.now(),
),
);
for (var userId in userIds) {
await _db
.collection(_options.usersCollectionName)
.doc(userId)
.collection('chats')
.doc(reference.id)
.set({'users': userIds});
}
chat.id = reference.id;
}
return chat;
}
} }

View file

@ -5,56 +5,40 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_storage/firebase_storage.dart'; import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';
import 'package:flutter_community_chat_firebase/config/firebase_chat_options.dart'; import 'package:flutter_community_chat_firebase/config/firebase_chat_options.dart';
import 'package:flutter_community_chat_firebase/dto/firebase_chat_document.dart';
import 'package:flutter_community_chat_firebase/dto/firebase_message_document.dart'; import 'package:flutter_community_chat_firebase/dto/firebase_message_document.dart';
import 'package:flutter_community_chat_firebase/service/firebase_chat_service.dart';
import 'package:flutter_community_chat_firebase/service/firebase_user_service.dart';
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart'; import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class FirebaseMessageService { class FirebaseMessageService implements MessageService {
FirebaseMessageService({ late final FirebaseFirestore _db;
required this.db, late final FirebaseStorage _storage;
required this.storage, late final ChatUserService _userService;
required this.userService, late FirebaseChatOptions _options;
required this.chatService,
required this.options,
});
final FirebaseFirestore db;
final FirebaseStorage storage;
final FirebaseUserService userService;
final FirebaseChatService chatService;
FirebaseChatOptions options;
late StreamController<List<ChatMessageModel>> _controller; late StreamController<List<ChatMessageModel>> _controller;
StreamSubscription<QuerySnapshot>? _subscription; StreamSubscription<QuerySnapshot>? _subscription;
ChatModel? _chat;
Future<void> setChat(ChatModel chat) async { FirebaseMessageService({
if (chat.id == null && chat is PersonalChatModel) { required ChatUserService userService,
var chatWithUser = await chatService.getChatByUser(chat.user); FirebaseApp? app,
FirebaseChatOptions? options,
}) {
var appInstance = app ?? Firebase.app();
if (chatWithUser != null) { _db = FirebaseFirestore.instanceFor(app: appInstance);
_chat = chatWithUser; _storage = FirebaseStorage.instanceFor(app: appInstance);
return; _userService = userService;
} _options = options ?? const FirebaseChatOptions();
}
_chat = chat;
} }
Future<void> _beforeSendMessage() async { Future<void> _sendMessage(ChatModel chat, Map<String, dynamic> data) async {
if (_chat != null) { var currentUser = await _userService.getCurrentUser();
_chat = await createChatIfNotExists(_chat!);
}
}
Future<void> _sendMessage(Map<String, dynamic> data) async { if (chat.id == null || currentUser == null) {
var currentUser = await userService.getCurrentUser();
if (_chat?.id == null || currentUser == null) {
return; return;
} }
@ -64,15 +48,15 @@ class FirebaseMessageService {
...data ...data
}; };
var chatReference = db var chatReference = _db
.collection( .collection(
options.chatsCollectionName, _options.chatsCollectionName,
) )
.doc(_chat!.id); .doc(chat.id);
await chatReference await chatReference
.collection( .collection(
options.messagesCollectionName, _options.messagesCollectionName,
) )
.add(message); .add(message);
@ -80,35 +64,54 @@ class FirebaseMessageService {
'last_used': DateTime.now(), 'last_used': DateTime.now(),
'last_message': message, 'last_message': message,
}); });
if (chat.id != null && _controller.hasListener && (_subscription == null)) {
_subscription = _startListeningForMessages(chat);
}
} }
Future<void> sendTextMessage(String text) => _beforeSendMessage().then( @override
(_) => _sendMessage({'text': text}), Future<void> sendTextMessage({
); required String text,
required ChatModel chat,
Future<void> sendImageMessage(Uint8List image) => _beforeSendMessage().then( }) =>
(_) { _sendMessage(
if (_chat?.id == null) { chat,
return null; {
} 'text': text,
var ref = storage.ref(
'${options.chatsCollectionName}/${_chat!.id}/${const Uuid().v4()}');
return ref.putData(image).then(
(_) => ref.getDownloadURL().then(
(url) {
_sendMessage({'image_url': url});
},
),
);
}, },
); );
Query<FirebaseMessageDocument> _getMessagesQuery(String chatId) => db @override
.collection(options.chatsCollectionName) Future<void> sendImageMessage({
.doc(chatId) required ChatModel chat,
.collection(options.messagesCollectionName) required Uint8List image,
}) async {
if (chat.id == null) {
return;
}
var ref = _storage
.ref('${_options.chatsCollectionName}/${chat.id}/${const Uuid().v4()}');
return ref.putData(image).then(
(_) => ref.getDownloadURL().then(
(url) {
_sendMessage(
chat,
{
'image_url': url,
},
);
},
),
);
}
Query<FirebaseMessageDocument> _getMessagesQuery(ChatModel chat) => _db
.collection(_options.chatsCollectionName)
.doc(chat.id)
.collection(_options.messagesCollectionName)
.orderBy('timestamp', descending: false) .orderBy('timestamp', descending: false)
.withConverter<FirebaseMessageDocument>( .withConverter<FirebaseMessageDocument>(
fromFirestore: (snapshot, _) => fromFirestore: (snapshot, _) =>
@ -116,15 +119,18 @@ class FirebaseMessageService {
toFirestore: (user, _) => user.toJson(), toFirestore: (user, _) => user.toJson(),
); );
Stream<List<ChatMessageModel>> getMessagesStream() { @override
Stream<List<ChatMessageModel>> getMessagesStream(ChatModel chat) {
_controller = StreamController<List<ChatMessageModel>>( _controller = StreamController<List<ChatMessageModel>>(
onListen: () { onListen: () {
if (_chat?.id != null) { if (chat.id != null) {
_subscription = _startListeningForMessages(_chat!); _subscription = _startListeningForMessages(chat);
} }
}, },
onCancel: () { onCancel: () {
_subscription?.cancel(); _subscription?.cancel();
_subscription = null;
debugPrint('Canceling messages stream');
}, },
); );
@ -132,7 +138,9 @@ class FirebaseMessageService {
} }
StreamSubscription<QuerySnapshot> _startListeningForMessages(ChatModel chat) { StreamSubscription<QuerySnapshot> _startListeningForMessages(ChatModel chat) {
var snapshots = _getMessagesQuery(chat.id!).snapshots(); debugPrint('Start listening for messages in chat ${chat.id}');
var snapshots = _getMessagesQuery(chat).snapshots();
return snapshots.listen( return snapshots.listen(
(snapshot) async { (snapshot) async {
@ -141,7 +149,7 @@ class FirebaseMessageService {
for (var messageDoc in snapshot.docs) { for (var messageDoc in snapshot.docs) {
var messageData = messageDoc.data(); var messageData = messageDoc.data();
var sender = await userService.getUser(messageData.sender); var sender = await _userService.getUser(messageData.sender);
if (sender != null) { if (sender != null) {
var timestamp = DateTime.fromMillisecondsSinceEpoch( var timestamp = DateTime.fromMillisecondsSinceEpoch(
@ -168,54 +176,4 @@ class FirebaseMessageService {
}, },
); );
} }
Future<ChatModel?> createChatIfNotExists(ChatModel chat) async {
if (chat.id == null) {
if (chat is! PersonalChatModel) {
return null;
}
var currentUser = await userService.getCurrentUser();
if (currentUser?.id == null || chat.user.id == null) {
return null;
}
List<String> userIds = [
currentUser!.id!,
chat.user.id!,
];
var reference = await db
.collection(options.chatsCollectionName)
.withConverter(
fromFirestore: (snapshot, _) =>
FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id),
toFirestore: (chat, _) => chat.toJson(),
)
.add(
FirebaseChatDocument(
personal: true,
users: userIds,
lastUsed: Timestamp.now(),
),
);
for (var userId in userIds) {
await db
.collection(options.usersCollectionName)
.doc(userId)
.collection('chats')
.doc(reference.id)
.set({'users': userIds});
}
chat.id = reference.id;
_subscription?.cancel();
_subscription = _startListeningForMessages(chat);
}
return chat;
}
} }

View file

@ -4,25 +4,32 @@
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_community_chat_firebase/config/firebase_chat_options.dart'; import 'package:flutter_community_chat_firebase/config/firebase_chat_options.dart';
import 'package:flutter_community_chat_firebase/dto/firebase_user_document.dart'; import 'package:flutter_community_chat_firebase/dto/firebase_user_document.dart';
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart'; import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
class FirebaseUserService { class FirebaseUserService implements ChatUserService {
FirebaseUserService({ FirebaseUserService({
required this.db, FirebaseApp? app,
required this.auth, FirebaseChatOptions? options,
required this.options, }) {
}); var appInstance = app ?? Firebase.app();
_db = FirebaseFirestore.instanceFor(app: appInstance);
_auth = FirebaseAuth.instanceFor(app: appInstance);
_options = options ?? const FirebaseChatOptions();
}
late FirebaseFirestore _db;
late FirebaseAuth _auth;
late FirebaseChatOptions _options;
FirebaseFirestore db;
FirebaseAuth auth;
FirebaseChatOptions options;
ChatUserModel? _currentUser; ChatUserModel? _currentUser;
final Map<String, ChatUserModel> _users = {}; final Map<String, ChatUserModel> _users = {};
CollectionReference<FirebaseUserDocument> get _userCollection => db CollectionReference<FirebaseUserDocument> get _userCollection => _db
.collection(options.usersCollectionName) .collection(_options.usersCollectionName)
.withConverter<FirebaseUserDocument>( .withConverter<FirebaseUserDocument>(
fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson( fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson(
snapshot.data()!, snapshot.data()!,
@ -31,6 +38,7 @@ class FirebaseUserService {
toFirestore: (user, _) => user.toJson(), toFirestore: (user, _) => user.toJson(),
); );
@override
Future<ChatUserModel?> getUser(String id) async { Future<ChatUserModel?> getUser(String id) async {
if (_users.containsKey(id)) { if (_users.containsKey(id)) {
return _users[id]!; return _users[id]!;
@ -54,11 +62,13 @@ class FirebaseUserService {
}); });
} }
@override
Future<ChatUserModel?> getCurrentUser() async => Future<ChatUserModel?> getCurrentUser() async =>
_currentUser == null && auth.currentUser?.uid != null _currentUser == null && _auth.currentUser?.uid != null
? _currentUser = await getUser(auth.currentUser!.uid) ? _currentUser = await getUser(_auth.currentUser!.uid)
: _currentUser; : _currentUser;
@override
Future<List<ChatUserModel>> getAllUsers() async { Future<List<ChatUserModel>> getAllUsers() async {
var currentUser = await getCurrentUser(); var currentUser = await getCurrentUser();
@ -67,13 +77,6 @@ class FirebaseUserService {
isNotEqualTo: currentUser?.id, isNotEqualTo: currentUser?.id,
); );
if (options.userFilter != null) {
query = query.where(
options.userFilter!.field,
isEqualTo: options.userFilter!.expectedValue,
);
}
var data = await query.get(); var data = await query.get();
return data.docs.map((user) { return data.docs.map((user) {

View file

@ -14,7 +14,7 @@ packages:
name: _flutterfire_internals name: _flutterfire_internals
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.9" version: "1.0.10"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
@ -84,21 +84,21 @@ packages:
name: cloud_firestore name: cloud_firestore
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.1.0" version: "4.2.0"
cloud_firestore_platform_interface: cloud_firestore_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: cloud_firestore_platform_interface name: cloud_firestore_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.9.0" version: "5.9.1"
cloud_firestore_web: cloud_firestore_web:
dependency: transitive dependency: transitive
description: description:
name: cloud_firestore_web name: cloud_firestore_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.0" version: "3.1.1"
code_builder: code_builder:
dependency: transitive dependency: transitive
description: description:
@ -154,28 +154,28 @@ packages:
name: firebase_auth name: firebase_auth
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.1.4" version: "4.2.1"
firebase_auth_platform_interface: firebase_auth_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_auth_platform_interface name: firebase_auth_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.11.3" version: "6.11.5"
firebase_auth_web: firebase_auth_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_auth_web name: firebase_auth_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.1.3" version: "5.2.1"
firebase_core: firebase_core:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_core name: firebase_core
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.3.0" version: "2.4.0"
firebase_core_platform_interface: firebase_core_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -189,28 +189,28 @@ packages:
name: firebase_core_web name: firebase_core_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.0.2"
firebase_storage: firebase_storage:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_storage name: firebase_storage
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "11.0.6" version: "11.0.8"
firebase_storage_platform_interface: firebase_storage_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_storage_platform_interface name: firebase_storage_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.1.24" version: "4.1.25"
firebase_storage_web: firebase_storage_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_storage_web name: firebase_storage_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.3.16" version: "3.3.17"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -228,7 +228,7 @@ packages:
description: description:
path: "packages/flutter_community_chat_interface" path: "packages/flutter_community_chat_interface"
ref: HEAD ref: HEAD
resolved-ref: "6a9e88ec8d07118e1543b1a82aa5482d6832cbf8" resolved-ref: c644e1affb1dd4f570bf0e4ae2e950f5e9d83c37
url: "https://github.com/Iconica-Development/flutter_community_chat.git" url: "https://github.com/Iconica-Development/flutter_community_chat.git"
source: git source: git
version: "0.0.1" version: "0.0.1"

View file

@ -4,11 +4,6 @@
library flutter_community_chat_interface; library flutter_community_chat_interface;
export 'src/community_chat_interface.dart'; export 'package:flutter_community_chat_interface/src/chat_data_provider.dart';
export 'src/model/chat.dart'; export 'package:flutter_community_chat_interface/src/model/model.dart';
export 'src/model/chat_image_message.dart'; export 'package:flutter_community_chat_interface/src/service/service.dart';
export 'src/model/chat_message.dart';
export 'src/model/chat_text_message.dart';
export 'src/model/chat_user.dart';
export 'src/model/group_chat.dart';
export 'src/model/personal_chat.dart';

View file

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter_community_chat_interface/flutter_community_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 ChatService chatService;
final MessageService messageService;
}

View file

@ -1,22 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'dart:typed_data';
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
import 'package:flutter_data_interface/flutter_data_interface.dart';
abstract class CommunityChatInterface extends DataInterface {
CommunityChatInterface() : super(token: _token);
static final Object _token = Object();
Future<void> setChat(ChatModel chat);
Future<void> sendTextMessage(String text);
Future<void> sendImageMessage(Uint8List image);
Stream<List<ChatMessageModel>> getMessagesStream();
Stream<List<ChatModel>> getChatsStream();
Future<List<ChatUserModel>> getChatUsers();
Future<void> deleteChat(ChatModel chat);
}

View file

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

View file

@ -0,0 +1,8 @@
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
abstract class ChatService {
Stream<List<PersonalChatModel>> getChatsStream();
Future<PersonalChatModel> getOrCreateChatByUser(ChatUserModel user);
Future<void> deleteChat(PersonalChatModel chat);
Future<PersonalChatModel> storeChatIfNot(PersonalChatModel chat);
}

View file

@ -0,0 +1,18 @@
import 'dart:typed_data';
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
abstract class MessageService {
Future<void> sendTextMessage({
required ChatModel chat,
required String text,
});
Future<void> sendImageMessage({
required ChatModel chat,
required Uint8List image,
});
Stream<List<ChatMessageModel>> getMessagesStream(
ChatModel chat,
);
}

View file

@ -0,0 +1,3 @@
export 'chat_service.dart';
export 'user_service.dart';
export 'message_service.dart';

View file

@ -0,0 +1,7 @@
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
abstract class ChatUserService {
Future<ChatUserModel?> getUser(String id);
Future<ChatUserModel?> getCurrentUser();
Future<List<ChatUserModel>> getAllUsers();
}

View file

@ -161,7 +161,7 @@ packages:
name: matcher name: matcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.13" version: "0.12.14"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
@ -196,7 +196,7 @@ packages:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.2" version: "1.8.3"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@ -257,7 +257,7 @@ packages:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.16" version: "0.4.17"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:

View file

@ -113,6 +113,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.1" version: "3.1.1"
cross_file:
dependency: transitive
description:
name: cross_file
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3+2"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@ -186,7 +193,7 @@ packages:
description: description:
path: "packages/flutter_community_chat_interface" path: "packages/flutter_community_chat_interface"
ref: HEAD ref: HEAD
resolved-ref: "6a9e88ec8d07118e1543b1a82aa5482d6832cbf8" resolved-ref: c644e1affb1dd4f570bf0e4ae2e950f5e9d83c37
url: "https://github.com/Iconica-Development/flutter_community_chat.git" url: "https://github.com/Iconica-Development/flutter_community_chat.git"
source: git source: git
version: "0.0.1" version: "0.0.1"
@ -206,6 +213,15 @@ packages:
url: "https://github.com/Iconica-Development/flutter_data_interface.git" url: "https://github.com/Iconica-Development/flutter_data_interface.git"
source: git source: git
version: "1.0.0" version: "1.0.0"
flutter_image_picker:
dependency: transitive
description:
path: "."
ref: "1.0.3"
resolved-ref: "20814755cca74296600a0ae3e016e46979e66a7e"
url: "https://github.com/Iconica-Development/flutter_image_picker"
source: git
version: "1.0.3"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -213,11 +229,23 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob: glob:
dependency: transitive dependency: transitive
description: description:
@ -239,6 +267,41 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.2" version: "4.0.2"
image_picker:
dependency: transitive
description:
name: image_picker
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.6"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.5+3"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.10"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.6+1"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.6.2"
intl: intl:
dependency: transitive dependency: transitive
description: description:
@ -246,6 +309,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.17.0" version: "0.17.0"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.4"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@ -425,7 +495,7 @@ packages:
name: sqflite name: sqflite
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.0+3" version: "2.2.2"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:

View file

@ -16,9 +16,9 @@ class ChatBottom extends StatefulWidget {
super.key, super.key,
}); });
final Future<void> Function(ChatModel chat, String text) onMessageSubmit; final Future<void> Function(String text) onMessageSubmit;
final TextInputBuilder messageInputBuilder; final TextInputBuilder messageInputBuilder;
final Function(ChatModel)? onPressSelectImage; final VoidCallback? onPressSelectImage;
final ChatModel chat; final ChatModel chat;
final ChatTranslations translations; final ChatTranslations translations;
@ -44,17 +44,16 @@ class _ChatBottomState extends State<ChatBottom> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (widget.onPressSelectImage != null) IconButton(
IconButton( onPressed: widget.onPressSelectImage,
onPressed: () => widget.onPressSelectImage!(widget.chat), icon: const Icon(Icons.image),
icon: const Icon(Icons.image), ),
),
IconButton( IconButton(
onPressed: () { onPressed: () {
var value = _textEditingController.text; var value = _textEditingController.text;
if (value.isNotEmpty) { if (value.isNotEmpty) {
widget.onMessageSubmit(widget.chat, value); widget.onMessageSubmit(value);
_textEditingController.clear(); _textEditingController.clear();
} }
}, },

View file

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_community_chat/flutter_community_chat.dart'; import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
SnackBar getImageLoadingSnackbar(ChatTranslations translations) => SnackBar( SnackBar getImageLoadingSnackbar(ChatTranslations translations) => SnackBar(
duration: const Duration(minutes: 1), duration: const Duration(minutes: 1),

View file

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart'; import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart'; import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
import 'package:flutter_community_chat_view/src/components/chat_image.dart'; import 'package:flutter_community_chat_view/src/components/chat_image.dart';
import 'package:flutter_image_picker/flutter_image_picker.dart';
class ChatOptions { class ChatOptions {
const ChatOptions({ const ChatOptions({
@ -13,7 +14,6 @@ class ChatOptions {
this.messageInputBuilder = _createMessageInput, this.messageInputBuilder = _createMessageInput,
this.chatRowContainerBuilder = _createChatRowContainer, this.chatRowContainerBuilder = _createChatRowContainer,
this.imagePickerContainerBuilder = _createImagePickerContainer, this.imagePickerContainerBuilder = _createImagePickerContainer,
this.closeImagePickerButtonBuilder = _createCloseImagePickerButton,
this.scaffoldBuilder = _createScaffold, this.scaffoldBuilder = _createScaffold,
this.userAvatarBuilder = _createUserAvatar, this.userAvatarBuilder = _createUserAvatar,
this.noChatsPlaceholderBuilder = _createNoChatsPlaceholder, this.noChatsPlaceholderBuilder = _createNoChatsPlaceholder,
@ -22,8 +22,7 @@ class ChatOptions {
final ButtonBuilder newChatButtonBuilder; final ButtonBuilder newChatButtonBuilder;
final TextInputBuilder messageInputBuilder; final TextInputBuilder messageInputBuilder;
final ContainerBuilder chatRowContainerBuilder; final ContainerBuilder chatRowContainerBuilder;
final ContainerBuilder imagePickerContainerBuilder; final ImagePickerContainerBuilder imagePickerContainerBuilder;
final ButtonBuilder closeImagePickerButtonBuilder;
final ScaffoldBuilder scaffoldBuilder; final ScaffoldBuilder scaffoldBuilder;
final UserAvatarBuilder userAvatarBuilder; final UserAvatarBuilder userAvatarBuilder;
final NoChatsPlaceholderBuilder noChatsPlaceholderBuilder; final NoChatsPlaceholderBuilder noChatsPlaceholderBuilder;
@ -70,23 +69,19 @@ Widget _createChatRowContainer(
); );
Widget _createImagePickerContainer( Widget _createImagePickerContainer(
Widget imagePicker, VoidCallback onClose,
ChatTranslations translations,
) => ) =>
Container( Container(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
color: Colors.black, color: Colors.black,
child: imagePicker, child: ImagePicker(
); customButton: ElevatedButton(
onPressed: onClose,
Widget _createCloseImagePickerButton( child: Text(
BuildContext context, translations.cancelImagePickerBtn,
VoidCallback onPressed, ),
ChatTranslations translations, ),
) =>
ElevatedButton(
onPressed: onPressed,
child: Text(
translations.cancelImagePickerBtn,
), ),
); );
@ -138,6 +133,11 @@ typedef ContainerBuilder = Widget Function(
Widget child, Widget child,
); );
typedef ImagePickerContainerBuilder = Widget Function(
VoidCallback onClose,
ChatTranslations translations,
);
typedef ScaffoldBuilder = Scaffold Function( typedef ScaffoldBuilder = Scaffold Function(
AppBar appBar, AppBar appBar,
Widget body, Widget body,

View file

@ -2,86 +2,114 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart'; import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart'; import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
import 'package:flutter_community_chat_view/src/components/chat_bottom.dart'; import 'package:flutter_community_chat_view/src/components/chat_bottom.dart';
import 'package:flutter_community_chat_view/src/components/chat_detail_row.dart'; import 'package:flutter_community_chat_view/src/components/chat_detail_row.dart';
import 'package:flutter_community_chat_view/src/components/image_loading_snackbar.dart';
class ChatDetailScreen extends StatelessWidget { class ChatDetailScreen extends StatelessWidget {
const ChatDetailScreen({ const ChatDetailScreen({
required this.options, required this.options,
required this.chat,
required this.onMessageSubmit, required this.onMessageSubmit,
required this.onUploadImage,
this.translations = const ChatTranslations(), this.translations = const ChatTranslations(),
this.chat,
this.chatMessages, this.chatMessages,
this.onPressSelectImage,
this.onPressChatTitle, this.onPressChatTitle,
super.key, super.key,
}); });
final ChatModel chat; final PersonalChatModel? chat;
final ChatOptions options; final ChatOptions options;
final ChatTranslations translations; final ChatTranslations translations;
final Stream<List<ChatMessageModel>>? chatMessages; final Stream<List<ChatMessageModel>>? chatMessages;
final Function(ChatModel)? onPressSelectImage; final Future<void> Function(Uint8List image) onUploadImage;
final Future<void> Function(ChatModel chat, String text) onMessageSubmit; final Future<void> Function(String text) onMessageSubmit;
final Future<void> Function(ChatModel chat)? onPressChatTitle; final VoidCallback? onPressChatTitle;
@override @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) {
appBar: AppBar( Future<void> onPressSelectImage() => showModalBottomSheet<Uint8List?>(
centerTitle: true, context: context,
title: GestureDetector( builder: (BuildContext context) =>
onTap: () => options.imagePickerContainerBuilder(
onPressChatTitle != null ? onPressChatTitle!(chat) : {}, () => Navigator.of(context).pop(),
child: Row( translations,
mainAxisSize: MainAxisSize.min, ),
children: [ ).then(
if (chat is PersonalChatModel) (image) async {
options.userAvatarBuilder( var messenger = ScaffoldMessenger.of(context)
(chat as PersonalChatModel).user, ..showSnackBar(
36.0, getImageLoadingSnackbar(translations),
), );
Expanded(
child: Padding( if (image != null) {
padding: const EdgeInsets.only(left: 15.5), await onUploadImage(image);
child: Text( }
(chat as PersonalChatModel).user.fullName,
style: const TextStyle(fontSize: 18), messenger.hideCurrentSnackBar();
},
);
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: GestureDetector(
onTap: onPressChatTitle,
child: Row(
mainAxisSize: MainAxisSize.min,
children: chat == null
? []
: [
options.userAvatarBuilder(
chat!.user,
36.0,
), ),
), Expanded(
), child: Padding(
], padding: const EdgeInsets.only(left: 15.5),
), child: Text(
chat!.user.fullName,
style: const TextStyle(fontSize: 18),
),
),
),
],
), ),
), ),
body: Column( ),
children: [ body: Column(
Expanded( children: [
child: StreamBuilder<List<ChatMessageModel>>( Expanded(
stream: chatMessages, child: StreamBuilder<List<ChatMessageModel>>(
builder: (BuildContext context, snapshot) => ListView( stream: chatMessages,
reverse: true, builder: (BuildContext context, snapshot) => ListView(
padding: const EdgeInsets.only(top: 24.0), reverse: true,
children: [ padding: const EdgeInsets.only(top: 24.0),
for (var message children: [
in (snapshot.data ?? chat.messages ?? []).reversed) for (var message
ChatDetailRow( in (snapshot.data ?? chat?.messages ?? []).reversed)
message: message, ChatDetailRow(
), message: message,
], ),
), ],
), ),
), ),
),
if (chat != null)
ChatBottom( ChatBottom(
chat: chat, chat: chat!,
messageInputBuilder: options.messageInputBuilder, messageInputBuilder: options.messageInputBuilder,
onPressSelectImage: onPressSelectImage, onPressSelectImage: onPressSelectImage,
onMessageSubmit: onMessageSubmit, onMessageSubmit: onMessageSubmit,
translations: translations, translations: translations,
), ),
], ],
), ),
); );
}
} }

View file

@ -20,10 +20,10 @@ class ChatScreen extends StatefulWidget {
final ChatOptions options; final ChatOptions options;
final ChatTranslations translations; final ChatTranslations translations;
final Stream<List<ChatModel>> chats; final Stream<List<PersonalChatModel>> chats;
final VoidCallback? onPressStartChat; final VoidCallback? onPressStartChat;
final void Function(ChatModel chat) onDeleteChat; final void Function(PersonalChatModel chat) onDeleteChat;
final void Function(ChatModel chat) onPressChat; final void Function(PersonalChatModel chat) onPressChat;
@override @override
State<ChatScreen> createState() => _ChatScreenState(); State<ChatScreen> createState() => _ChatScreenState();
@ -43,11 +43,11 @@ class _ChatScreenState extends State<ChatScreen> {
child: ListView( child: ListView(
padding: const EdgeInsets.only(top: 15.0), padding: const EdgeInsets.only(top: 15.0),
children: [ children: [
StreamBuilder<List<ChatModel>>( StreamBuilder<List<PersonalChatModel>>(
stream: widget.chats, stream: widget.chats,
builder: (BuildContext context, snapshot) => Column( builder: (BuildContext context, snapshot) => Column(
children: [ children: [
for (ChatModel chat in snapshot.data ?? []) for (PersonalChatModel chat in snapshot.data ?? [])
Builder( Builder(
builder: (context) => Dismissible( builder: (context) => Dismissible(
confirmDismiss: (_) => showDialog( confirmDismiss: (_) => showDialog(
@ -100,15 +100,11 @@ class _ChatScreenState extends State<ChatScreen> {
onTap: () => widget.onPressChat(chat), onTap: () => widget.onPressChat(chat),
child: widget.options.chatRowContainerBuilder( child: widget.options.chatRowContainerBuilder(
ChatRow( ChatRow(
avatar: chat is PersonalChatModel avatar: widget.options.userAvatarBuilder(
? widget.options.userAvatarBuilder( chat.user,
chat.user, 40.0,
40.0, ),
) title: chat.user.fullName,
: Container(),
title: chat is PersonalChatModel
? chat.user.fullName
: (chat as GroupChatModel).title,
subTitle: chat.lastMessage != null subTitle: chat.lastMessage != null
? chat.lastMessage ? chat.lastMessage
is ChatTextMessageModel is ChatTextMessageModel

View file

@ -113,6 +113,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.1" version: "3.1.1"
cross_file:
dependency: transitive
description:
name: cross_file
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3+2"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@ -179,7 +186,7 @@ packages:
description: description:
path: "packages/flutter_community_chat_interface" path: "packages/flutter_community_chat_interface"
ref: HEAD ref: HEAD
resolved-ref: "6a9e88ec8d07118e1543b1a82aa5482d6832cbf8" resolved-ref: c644e1affb1dd4f570bf0e4ae2e950f5e9d83c37
url: "https://github.com/Iconica-Development/flutter_community_chat.git" url: "https://github.com/Iconica-Development/flutter_community_chat.git"
source: git source: git
version: "0.0.1" version: "0.0.1"
@ -192,6 +199,15 @@ packages:
url: "https://github.com/Iconica-Development/flutter_data_interface.git" url: "https://github.com/Iconica-Development/flutter_data_interface.git"
source: git source: git
version: "1.0.0" version: "1.0.0"
flutter_image_picker:
dependency: "direct main"
description:
path: "."
ref: "1.0.3"
resolved-ref: "20814755cca74296600a0ae3e016e46979e66a7e"
url: "https://github.com/Iconica-Development/flutter_image_picker"
source: git
version: "1.0.3"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -199,11 +215,23 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob: glob:
dependency: transitive dependency: transitive
description: description:
@ -225,6 +253,41 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.2" version: "4.0.2"
image_picker:
dependency: transitive
description:
name: image_picker
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.6"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.5+3"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.10"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.6+1"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.6.2"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@ -232,6 +295,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.17.0" version: "0.17.0"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.4"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@ -411,7 +481,7 @@ packages:
name: sqflite name: sqflite
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.0+3" version: "2.2.2"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:

View file

@ -21,7 +21,10 @@ dependencies:
url: https://github.com/Iconica-Development/flutter_community_chat.git url: https://github.com/Iconica-Development/flutter_community_chat.git
path: packages/flutter_community_chat_interface path: packages/flutter_community_chat_interface
cached_network_image: ^3.2.2 cached_network_image: ^3.2.2
flutter_image_picker:
git:
url: https://github.com/Iconica-Development/flutter_image_picker
ref: 1.0.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter