Merge pull request #1 from Iconica-Development/feature/open-chat-after-creating-new-one

feat: create chat after sending first message
This commit is contained in:
Stein Milder 2022-11-08 17:10:16 +01:00 committed by GitHub
commit 01ab5599d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 241 additions and 214 deletions

View file

@ -38,26 +38,32 @@ class CommunityChat extends StatelessWidget {
NewChatScreen(
options: options,
translations: translations,
onPressCreateChat: (user) => dataProvider.createChat(
PersonalChatModel(user: user),
),
onPressCreateChat: (user) {
_onPressChat(
context,
PersonalChatModel(user: user),
);
},
users: users,
),
));
Future<void> _onPressChat(BuildContext context, ChatModel chat) => _push(
context,
ChatDetailScreen(
options: options,
translations: translations,
chat: chat,
chatMessages: dataProvider.getMessagesStream(chat),
onPressSelectImage: (ChatModel chat) =>
_onPressSelectImage(context, chat),
onMessageSubmit: (ChatModel chat, String content) =>
dataProvider.sendTextMessage(chat, content),
),
);
Future<void> _onPressChat(BuildContext context, ChatModel chat) async {
dataProvider.setChat(chat);
_push(
context,
ChatDetailScreen(
options: options,
translations: translations,
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?>(
@ -76,7 +82,7 @@ class CommunityChat extends StatelessWidget {
).then(
(image) {
if (image != null) {
return dataProvider.sendImageMessage(chat, image);
return dataProvider.sendImageMessage(image);
}
},
);

View file

@ -179,7 +179,7 @@ packages:
description:
path: "packages/flutter_community_chat_interface"
ref: HEAD
resolved-ref: bfca7ca229a0f9e6749d079a5b447e50bef6d56f
resolved-ref: "7b03c934cfa28fc5d85f3d59974bb0757a439911"
url: "https://github.com/Iconica-Development/flutter_community_chat.git"
source: git
version: "0.0.1"
@ -188,7 +188,7 @@ packages:
description:
path: "packages/flutter_community_chat_view"
ref: HEAD
resolved-ref: "4af8360850b7d8d220f3052e4be25d6eb45ef963"
resolved-ref: "7b03c934cfa28fc5d85f3d59974bb0757a439911"
url: "https://github.com/Iconica-Development/flutter_community_chat.git"
source: git
version: "0.0.1"

View file

@ -13,12 +13,12 @@ dependencies:
sdk: flutter
flutter_community_chat_view:
git:
url: https://github.com/Iconica-Development/flutter_community_chat.git
path: packages/flutter_community_chat_view
url: https://github.com/Iconica-Development/flutter_community_chat.git
path: packages/flutter_community_chat_view
flutter_community_chat_interface:
git:
url: https://github.com/Iconica-Development/flutter_community_chat.git
path: packages/flutter_community_chat_interface
url: https://github.com/Iconica-Development/flutter_community_chat.git
path: packages/flutter_community_chat_interface
flutter_image_picker:
git:
url: https://github.com/Iconica-Development/flutter_image_picker

View file

@ -38,38 +38,40 @@ class FirebaseCommunityChatDataProvider extends CommunityChatInterface {
options: firebaseChatOptoons,
);
_messageService = FirebaseMessageService(
db: db,
storage: storage,
userService: _userService,
options: firebaseChatOptoons,
);
_chatService = FirebaseChatService(
db: db,
userService: _userService,
options: firebaseChatOptoons,
);
_messageService = FirebaseMessageService(
db: db,
storage: storage,
userService: _userService,
chatService: _chatService,
options: firebaseChatOptoons,
);
}
@override
Stream<List<ChatMessageModel>> getMessagesStream(ChatModel chat) =>
_messageService.getMessagesStream(chat);
Stream<List<ChatMessageModel>> getMessagesStream() =>
_messageService.getMessagesStream();
@override
Future<List<ChatUserModel>> getChatUsers() => _userService.getNewUsers();
Future<List<ChatUserModel>> getChatUsers() => _userService.getAllUsers();
@override
Stream<List<ChatModel>> getChatsStream() => _chatService.getChatsStream();
@override
Future<void> createChat(ChatModel chat) => _chatService.createChat(chat);
Future<void> sendTextMessage(String text) =>
_messageService.sendTextMessage(text);
@override
Future<void> sendTextMessage(ChatModel chat, String text) =>
_messageService.sendTextMessage(chat, text);
Future<void> sendImageMessage(Uint8List image) =>
_messageService.sendImageMessage(image);
@override
Future<void> sendImageMessage(ChatModel chat, Uint8List image) =>
_messageService.sendImageMessage(chat, image);
Future<void> setChat(ChatModel chat) async =>
await _messageService.setChat(chat);
}

View file

@ -197,43 +197,28 @@ class FirebaseChatService {
return controller.stream;
}
Future<void> createChat(ChatModel chat) async {
if (chat is! PersonalChatModel) {
return;
}
Future<ChatModel?> getChatByUser(ChatUserModel user) async {
var currentUser = await userService.getCurrentUser();
var chatCollection = await db
.collection(options.usersCollectionName)
.doc(currentUser?.id)
.collection('chats')
.get();
if (currentUser?.id == null || chat.user.id == null) {
return;
for (var element in chatCollection.docs) {
var data = element.data();
if (data.containsKey('id') &&
data.containsKey('users') &&
data['users'] is List) {
if (data['users'].contains(user.id)) {
return PersonalChatModel(
id: data['id'],
user: user,
);
}
}
}
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')
.add({'id': reference.id});
}
return null;
}
}

View file

@ -4,11 +4,12 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:cloud_firestore/cloud_firestore.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/dto/firebase_chat_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:uuid/uuid.dart';
@ -18,37 +19,35 @@ class FirebaseMessageService {
required this.db,
required this.storage,
required this.userService,
required this.chatService,
required this.options,
});
final FirebaseFirestore db;
final FirebaseStorage storage;
final FirebaseUserService userService;
final FirebaseChatService chatService;
FirebaseChatOptoons options;
late StreamController<List<ChatMessageModel>> _controller;
StreamSubscription<QuerySnapshot>? _subscription;
ChatModel? _chat;
Future<void> sendTextMessage(ChatModel chat, String text) =>
_sendMessage(chat, {'text': text});
Future<void> sendImageMessage(ChatModel chat, Uint8List image) async {
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});
},
),
);
Future<void> setChat(ChatModel chat) async {
if (chat is PersonalChatModel) {
_chat = await chatService.getChatByUser(chat.user) ?? chat;
}
}
Future<void> _sendMessage(
ChatModel chat,
Map<String, dynamic> data,
) async {
Future<void> _beforeSendMessage() async {
if (_chat != null) {
_chat = await createChatIfNotExists(_chat!);
}
}
Future<void> _sendMessage(Map<String, dynamic> data) async {
var currentUser = await userService.getCurrentUser();
if (currentUser == null) {
if (_chat?.id == null || currentUser == null) {
return;
}
@ -58,9 +57,17 @@ class FirebaseMessageService {
...data
};
var chatReference = db.collection(options.chatsCollectionName).doc(chat.id);
var chatReference = db
.collection(
options.chatsCollectionName,
)
.doc(_chat!.id);
await chatReference.collection(options.messagesCollectionName).add(message);
await chatReference
.collection(
options.messagesCollectionName,
)
.add(message);
await chatReference.update({
'last_used': DateTime.now(),
@ -68,6 +75,29 @@ class FirebaseMessageService {
});
}
Future<void> sendTextMessage(String text) => _beforeSendMessage().then(
(_) => _sendMessage({'text': text}),
);
Future<void> sendImageMessage(Uint8List image) => _beforeSendMessage().then(
(_) {
if (_chat?.id == null) {
return null;
}
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
.collection(options.chatsCollectionName)
.doc(chatId)
@ -79,49 +109,103 @@ class FirebaseMessageService {
toFirestore: (user, _) => user.toJson(),
);
Stream<List<ChatMessageModel>> getMessagesStream(ChatModel chat) {
late StreamController<List<ChatMessageModel>> controller;
StreamSubscription<QuerySnapshot>? subscription;
controller = StreamController<List<ChatMessageModel>>(
Stream<List<ChatMessageModel>> getMessagesStream() {
_controller = StreamController<List<ChatMessageModel>>(
onListen: () {
var snapshots = _getMessagesQuery(chat.id!).snapshots();
subscription = snapshots.listen(
(snapshot) async {
List<ChatMessageModel> messages = [];
for (var messageDoc in snapshot.docs) {
var messageData = messageDoc.data();
var sender = await userService.getUser(messageData.sender);
if (sender != null) {
var timestamp = DateTime.fromMillisecondsSinceEpoch(
(messageData.timestamp).millisecondsSinceEpoch,
);
messages.add(
messageData.imageUrl != null
? ChatImageMessageModel(
sender: sender,
imageUrl: messageData.imageUrl!,
timestamp: timestamp,
)
: ChatTextMessageModel(
sender: sender,
text: messageData.text!,
timestamp: timestamp,
),
);
}
}
controller.add(messages);
},
);
if (_chat?.id != null) {
_subscription = _startListeningForMessages(_chat!);
}
},
onCancel: () => subscription?.cancel(),
onCancel: () => _subscription?.cancel(),
);
return controller.stream;
return _controller.stream;
}
StreamSubscription<QuerySnapshot> _startListeningForMessages(ChatModel chat) {
var snapshots = _getMessagesQuery(chat.id!).snapshots();
return snapshots.listen(
(snapshot) async {
List<ChatMessageModel> messages = [];
for (var messageDoc in snapshot.docs) {
var messageData = messageDoc.data();
var sender = await userService.getUser(messageData.sender);
if (sender != null) {
var timestamp = DateTime.fromMillisecondsSinceEpoch(
(messageData.timestamp).millisecondsSinceEpoch,
);
messages.add(
messageData.imageUrl != null
? ChatImageMessageModel(
sender: sender,
imageUrl: messageData.imageUrl!,
timestamp: timestamp,
)
: ChatTextMessageModel(
sender: sender,
text: messageData.text!,
timestamp: timestamp,
),
);
}
}
_controller.add(messages);
},
);
}
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')
.add({'id': reference.id, 'users': userIds});
}
chat.id = reference.id;
_subscription?.cancel();
_subscription = _startListeningForMessages(chat);
}
return chat;
}
}

View file

@ -5,7 +5,6 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.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_user_document.dart';
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
@ -19,7 +18,6 @@ class FirebaseUserService {
FirebaseFirestore db;
FirebaseAuth auth;
FirebaseChatOptoons options;
ChatUserModel? _currentUser;
final Map<String, ChatUserModel> _users = {};
@ -33,16 +31,6 @@ class FirebaseUserService {
toFirestore: (user, _) => user.toJson(),
);
CollectionReference<FirebaseChatDocument> get _chatsCollection => db
.collection(options.chatsCollectionName)
.withConverter<FirebaseChatDocument>(
fromFirestore: (snapshot, _) => FirebaseChatDocument.fromJson(
snapshot.data()!,
snapshot.id,
),
toFirestore: (chat, _) => chat.toJson(),
);
Future<ChatUserModel?> getUser(String id) async {
if (_users.containsKey(id)) {
return _users[id]!;
@ -65,46 +53,17 @@ class FirebaseUserService {
});
}
Future<ChatUserModel?> getCurrentUser() async {
return _currentUser == null && auth.currentUser?.uid != null
? _currentUser = await getUser(auth.currentUser!.uid)
: _currentUser;
}
Future<ChatUserModel?> getCurrentUser() async =>
_currentUser == null && auth.currentUser?.uid != null
? _currentUser = await getUser(auth.currentUser!.uid)
: _currentUser;
Future<List<ChatUserModel>> getNewUsers() async {
Future<List<ChatUserModel>> getAllUsers() async {
var currentUser = await getCurrentUser();
var existingUserIds = [];
var existingChatCollection = await db
.collection(options.usersCollectionName)
.doc(currentUser?.id)
.collection('chats')
.get();
var existingChatsIds =
existingChatCollection.docs.map((chat) => chat['id']).toList();
if (existingChatsIds.isNotEmpty) {
for (var existingChatsId in existingChatsIds) {
var existingChat = await _chatsCollection.doc(existingChatsId).get();
var existingChatData = existingChat.data();
if (existingChatData != null) {
existingUserIds.addAll(
existingChatData.users,
);
}
}
}
var data = await _userCollection.get();
return data.docs
.where(
(user) =>
user.id != currentUser?.id && !existingUserIds.contains(user.id),
)
.map((user) {
return data.docs.where((user) => user.id != currentUser?.id).map((user) {
var userData = user.data();
return ChatUserModel(
id: user.id,

View file

@ -226,11 +226,9 @@ packages:
flutter_community_chat_interface:
dependency: "direct main"
description:
path: "packages/flutter_community_chat_interface"
ref: HEAD
resolved-ref: "43805605932f617103af8895c7bc57703a7fbac7"
url: "https://github.com/Iconica-Development/flutter_community_chat.git"
source: git
path: "/Users/steinmilder/documents/packages/chat/flutter_community_chat/packages/flutter_community_chat_interface"
relative: false
source: path
version: "0.0.1"
flutter_data_interface:
dependency: transitive

View file

@ -16,8 +16,8 @@ dependencies:
sdk: flutter
flutter_community_chat_interface:
git:
url: https://github.com/Iconica-Development/flutter_community_chat.git
path: packages/flutter_community_chat_interface
url: https://github.com/Iconica-Development/flutter_community_chat.git
path: packages/flutter_community_chat_interface
firebase_auth: ^3.11.2
dev_dependencies:

View file

@ -12,10 +12,10 @@ abstract class CommunityChatInterface extends DataInterface {
static final Object _token = Object();
Future<void> createChat(ChatModel chat);
Future<void> sendTextMessage(ChatModel chat, String text);
Future<void> sendImageMessage(ChatModel chat, Uint8List image);
Stream<List<ChatMessageModel>> getMessagesStream(ChatModel chat);
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();
}

View file

@ -5,15 +5,15 @@
import 'package:flutter_community_chat_interface/src/model/chat_message.dart';
abstract class ChatModel {
const ChatModel({
ChatModel({
this.id,
this.messages = const [],
this.lastUsed,
this.lastMessage,
});
final String? id;
final List<ChatMessageModel>? messages;
final DateTime? lastUsed;
final ChatMessageModel? lastMessage;
String? id;
List<ChatMessageModel>? messages;
DateTime? lastUsed;
ChatMessageModel? lastMessage;
}

View file

@ -184,11 +184,9 @@ packages:
flutter_community_chat_interface:
dependency: transitive
description:
path: "packages/flutter_community_chat_interface"
ref: HEAD
resolved-ref: "43805605932f617103af8895c7bc57703a7fbac7"
url: "https://github.com/Iconica-Development/flutter_community_chat.git"
source: git
path: "/Users/steinmilder/documents/packages/chat/flutter_community_chat/packages/flutter_community_chat_interface"
relative: false
source: path
version: "0.0.1"
flutter_community_chat_view:
dependency: "direct main"

View file

@ -86,10 +86,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
title: user.name ?? '',
),
),
onTap: () {
widget.onPressCreateChat(user);
Navigator.of(context).pop();
},
onTap: () => widget.onPressCreateChat(user),
),
],
),

View file

@ -177,11 +177,9 @@ packages:
flutter_community_chat_interface:
dependency: "direct main"
description:
path: "packages/flutter_community_chat_interface"
ref: HEAD
resolved-ref: "43805605932f617103af8895c7bc57703a7fbac7"
url: "https://github.com/Iconica-Development/flutter_community_chat.git"
source: git
path: "/Users/steinmilder/documents/packages/chat/flutter_community_chat/packages/flutter_community_chat_interface"
relative: false
source: path
version: "0.0.1"
flutter_data_interface:
dependency: transitive

View file

@ -18,8 +18,8 @@ dependencies:
intl: ^0.17.0
flutter_community_chat_interface:
git:
url: https://github.com/Iconica-Development/flutter_community_chat.git
path: packages/flutter_community_chat_interface
url: https://github.com/Iconica-Development/flutter_community_chat.git
path: packages/flutter_community_chat_interface
cached_network_image: ^3.2.2
dev_dependencies: