Merge pull request #39 from Iconica-Development/bugfix/poi

fix: service naming, image loading indicator
This commit is contained in:
Gorter-dev 2024-01-17 15:45:37 +01:00 committed by GitHub
commit 28e307cf90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
81 changed files with 1400 additions and 1152 deletions

View file

@ -2,22 +2,21 @@ version: 2
updates:
- package-ecosystem: "pub"
directory: "/packages/flutter_community_chat"
directory: "/packages/flutter_chat"
schedule:
interval: "weekly"
- package-ecosystem: "pub"
directory: "/packages/flutter_community_chat_firebase"
directory: "/packages/flutter_chat_firebase"
schedule:
interval: "weekly"
- package-ecosystem: "pub"
directory: "/packages/flutter_community_chat_interface"
directory: "/packages/flutter_chat_interface"
schedule:
interval: "weekly"
- package-ecosystem: "pub"
directory: "/packages/flutter_community_chat_view"
directory: "/packages/flutter_chat_view"
schedule:
interval: "weekly"

8
.gitignore vendored
View file

@ -37,9 +37,9 @@ build/
.metadata
pubspec.lock
packages/flutter_community_chat/pubspec.lock
packages/flutter_community_chat_firebase/pubspec.lock
packages/flutter_community_chat_interface/pubspec.lock
packages/flutter_community_chat_view/pubspec.lock
packages/flutter_chat/pubspec.lock
packages/flutter_chat_firebase/pubspec.lock
packages/flutter_chat_interface/pubspec.lock
packages/flutter_chat_view/pubspec.lock
pubspec_overrides.yaml

View file

@ -1,34 +1,52 @@
# Flutter Community Chat
# Flutter Chat
Flutter Community Chat is a package which gives the possibility to add a (personal or group) chat to your Flutter-application. Default this package adds support for a Firebase back-end. You can add your custom back-end (like a Websocket-API) by extending the `CommunityChatInterface` interface from the `flutter_community_chat_interface` package.
Flutter Chat is a package which gives the possibility to add a (personal or group) chat to your Flutter-application. Default this package adds support for a Firebase back-end. You can add your custom back-end (like a Websocket-API) by extending the `ChatInterface` interface from the `flutter_chat_interface` package.
![Flutter Community Chat GIF](example.gif)
![Flutter Chat GIF](example.gif)
Figma Design that defines this component (only accessible for Iconica developers): https://www.figma.com/file/4WkjwynOz5wFeFBRqTHPeP/Iconica-Design-System?type=design&node-id=357%3A3342&mode=design&t=XulkAJNPQ32ARxWh-1
Figma clickable prototype that demonstrates this component (only accessible for Iconica developers): https://www.figma.com/proto/PRJoVXQ5aOjAICfkQdAq2A/Iconica-User-Stories?page-id=1%3A2&type=design&node-id=56-6837&viewport=279%2C2452%2C0.2&t=E7Al3Xng2WXnbCEQ-1&scaling=scale-down&starting-point-node-id=56%3A6837&mode=design
## Setup
To use this package, add flutter_community_chat as a dependency in your pubspec.yaml file:
To use this package, add flutter_chat as a dependency in your pubspec.yaml file:
```
flutter_community_chat:
flutter_chat:
git:
url: https://github.com/Iconica-Development/flutter_community_chat.git
path: packages/flutter_community_chat
url: https://github.com/Iconica-Development/flutter_chat
path: packages/flutter_chat
```
If you are going to use Firebase as the back-end of the Community Chat, you should also add the following package as a dependency to your pubspec.yaml file:
```
flutter_community_chat_firebase:
flutter_chat_firebase:
git:
url: https://github.com/Iconica-Development/flutter_community_chat.git
path: packages/flutter_community_chat_firebase
url: https://github.com/Iconica-Development/flutter_chat
path: packages/flutter_chat_firebase
```
Create a Firebase project for your application and add firebase firestore and storage.
To use the camera or photo library to send photos add the following to your project:
For ios add the following lines to your info.plist:
```
<key>NSCameraUsageDescription</key>
<string>Access camera</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Library</string>
```
For android add the following lines to your AndroidManifest.xml:
```
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.GALLERY"/>
```
## How to use
To use the module within your Flutter-application with predefined `Go_router` routes you should add the following:
@ -37,18 +55,15 @@ Add go_router as dependency to your project.
Add the following configuration to your flutter_application:
```
List<GoRoute> getCommunityChatRoutes() => getCommunityChatStoryRoutes(
CommunityChatUserStoryConfiguration(
service: FirebaseChatService(userService: FirebaseUserService()),
userService: FirebaseUserService(),
messageService:
FirebaseMessageService(userService: FirebaseUserService()),
List<GoRoute> getChatRoutes() => getChatStoryRoutes(
ChatUserStoryConfiguration(
chatService: chatService,
chatOptionsBuilder: (ctx) => const ChatOptions(),
),
);
```
Add the `getCommunityChatRoutes()` to your go_router routes like so:
Add the `getChatRoutes()` to your go_router routes like so:
```
final GoRouter _router = GoRouter(
@ -61,13 +76,14 @@ final GoRouter _router = GoRouter(
);
},
),
...getCommunityChatRoutes()
...getChatRoutes()
],
);
```
To use the module within your Flutter-application without predefined `Go_router` routes add the following code to the build-method of a chosen widget:
The `ChatScreen` shows all chats that you currently have with their latest messages.
To add the `ChatScreen` add the following code:
````
@ -81,6 +97,7 @@ ChatScreen(
);
```
The `ChatDetailScreen` shows the messages that are in the current chat you selected.
To add the `ChatDetailScreen` add the following code:
```
@ -90,12 +107,10 @@ ChatDetailScreen(
onUploadImage: onUploadImage,
onReadChat: onReadChat,
service: service,
chatUserService: chatUserService,
messageService: messageService,
pageSize: pageSize,
);
```
On the `NewChatScreen` you can select a person to chat.
To add the `NewChatScreen` add the following code:
```
@ -103,7 +118,20 @@ NewChatScreen(
options: options,
onPressCreateChat: onPressCreateChat,
service: service,
userService: userService,
);
```
The `ChatEntryWidget` is a widget you can put anywhere in your app.
It displays the amount of unread messages you currently have.
You can choose to add a onTap to the `ChatEntryWidget` so it routes to the `ChatScreen`,
where all your chats are shown.
To add the `ChatEntryWidget` add the follwoing code:
```
ChatEntryWidget(
chatService: chatService,
onTap: onTap,
);
```
@ -121,13 +149,13 @@ The `ImagePickerTheme` also has its own parameters, how to use these parameters
## Issues
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_community_chat/pulls) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_chat/pulls) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
## Want to contribute
If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](../CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_community_chat/pulls).
If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](../CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_chat/pulls).
## Author
This `flutter_community_chat` for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>
This `flutter_chat` for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>
````

View file

@ -1,4 +1,4 @@
name: flutter_community_chat
name: flutter_chat
packages:
- packages/**
@ -21,7 +21,7 @@ scripts:
run: melos exec -c 1 -- "flutter pub upgrade"
create:
# run create in the example folder of flutter_community_chat_view
# run create in the example folder of flutter_chat_view
run: melos exec --scope="*example*" -c 1 -- "flutter create ."
analyze:

View file

@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
library flutter_chat;
export 'package:flutter_chat_view/flutter_chat_view.dart';
export 'package:flutter_chat_interface/flutter_chat_interface.dart';
export 'package:flutter_chat/src/routes.dart';
export 'package:flutter_chat/src/models/chat_configuration.dart';
export 'package:flutter_chat/src/flutter_chat_userstory.dart';

View file

@ -3,40 +3,35 @@
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_community_chat/src/models/community_chat_configuration.dart';
import 'package:flutter_community_chat/src/go_router.dart';
import 'package:flutter_community_chat/src/routes.dart';
import 'package:flutter_community_chat_view/flutter_community_chat_view.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> getCommunityChatStoryRoutes(
CommunityChatUserStoryConfiguration configuration,
List<GoRoute> getChatStoryRoutes(
ChatUserStoryConfiguration configuration,
) =>
<GoRoute>[
GoRoute(
path: CommunityChatUserStoryRoutes.chatScreen,
path: ChatUserStoryRoutes.chatScreen,
pageBuilder: (context, state) {
var chatScreen = ChatScreen(
pageSize: configuration.pageSize,
service: configuration.service,
service: configuration.chatService,
options: configuration.chatOptionsBuilder(context),
onNoChats: () async =>
await context.push(CommunityChatUserStoryRoutes.newChatScreen),
await context.push(ChatUserStoryRoutes.newChatScreen),
onPressStartChat: () async {
if (configuration.onPressStartChat != null) {
return await configuration.onPressStartChat?.call();
}
return await context
.push(CommunityChatUserStoryRoutes.newChatScreen);
return await context.push(ChatUserStoryRoutes.newChatScreen);
},
onPressChat: (chat) =>
configuration.onPressChat?.call(context, chat) ??
context.push(
CommunityChatUserStoryRoutes.chatDetailViewPath(chat.id!)),
context.push(ChatUserStoryRoutes.chatDetailViewPath(chat.id!)),
onDeleteChat: (chat) =>
configuration.onDeleteChat?.call(context, chat) ??
configuration.service.deleteChat(chat),
configuration.chatService.chatOverviewService.deleteChat(chat),
deleteChatDialog: configuration.deleteChatDialog,
translations: configuration.translations,
);
@ -54,35 +49,38 @@ List<GoRoute> getCommunityChatStoryRoutes(
},
),
GoRoute(
path: CommunityChatUserStoryRoutes.chatDetailScreen,
path: ChatUserStoryRoutes.chatDetailScreen,
pageBuilder: (context, state) {
var chatId = state.pathParameters['id'];
var chat = PersonalChatModel(user: ChatUserModel(), id: chatId);
var chatDetailScreen = ChatDetailScreen(
pageSize: configuration.messagePageSize,
options: configuration.chatOptionsBuilder(context),
translations: configuration.translations,
chatUserService: configuration.userService,
service: configuration.service,
messageService: configuration.messageService,
chat: chat,
service: configuration.chatService,
chatId: chatId!,
onMessageSubmit: (message) async {
configuration.onMessageSubmit?.call(message) ??
configuration.messageService
.sendTextMessage(chat: chat, text: message);
configuration.afterMessageSent?.call(chat);
configuration.chatService.chatDetailService
.sendTextMessage(chatId: chatId, text: message);
configuration.afterMessageSent?.call(chatId);
},
onUploadImage: (image) async {
configuration.onUploadImage?.call(image) ??
configuration.messageService
.sendImageMessage(chat: chat, image: image);
configuration.afterMessageSent?.call(chat);
configuration.chatService.chatDetailService
.sendImageMessage(chatId: chatId, image: image);
configuration.afterMessageSent?.call(chatId);
},
onReadChat: (chat) =>
configuration.onReadChat?.call(chat) ??
configuration.service.readChat(chat),
onPressChatTitle: (context, chat) =>
configuration.onPressChatTitle?.call(context, chat),
configuration.chatService.chatOverviewService.readChat(chat),
onPressChatTitle: (context, chat) {
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(
@ -99,19 +97,20 @@ List<GoRoute> getCommunityChatStoryRoutes(
},
),
GoRoute(
path: CommunityChatUserStoryRoutes.newChatScreen,
path: ChatUserStoryRoutes.newChatScreen,
pageBuilder: (context, state) {
var newChatScreen = NewChatScreen(
options: configuration.chatOptionsBuilder(context),
translations: configuration.translations,
service: configuration.service,
userService: configuration.userService,
service: configuration.chatService,
onPressCreateChat: (user) async {
configuration.onPressCreateChat?.call(user);
if (configuration.onPressChat != null) return;
var chat = await configuration.service.getChatByUser(user);
var chat = await configuration.chatService.chatOverviewService
.getChatByUser(user);
if (chat.id == null) {
chat = await configuration.service.storeChatIfNot(
chat = await configuration.chatService.chatOverviewService
.storeChatIfNot(
PersonalChatModel(
user: user,
),
@ -119,8 +118,7 @@ List<GoRoute> getCommunityChatStoryRoutes(
}
if (context.mounted) {
await context.push(
CommunityChatUserStoryRoutes.chatDetailViewPath(
chat.id ?? ''));
ChatUserStoryRoutes.chatDetailViewPath(chat.id ?? ''));
}
});
return buildScreenWithoutTransition(
@ -136,4 +134,38 @@ List<GoRoute> getCommunityChatStoryRoutes(
);
},
),
GoRoute(
path: ChatUserStoryRoutes.chatProfileScreen,
pageBuilder: (context, state) {
var chatId = state.pathParameters['id'];
var userId = state.pathParameters['userId'];
var id = userId == 'null' ? null : userId;
var profileScreen = ChatProfileScreen(
translations: configuration.translations,
chatService: configuration.chatService,
chatId: chatId!,
userId: id,
onTapUser: (user) async {
if (configuration.onPressUserProfile != null) {
return configuration.onPressUserProfile!.call();
}
return await context.push(
ChatUserStoryRoutes.chatProfileScreenPath(chatId, user),
);
},
);
return buildScreenWithoutTransition(
context: context,
state: state,
child: configuration.chatPageBuilder?.call(
context,
profileScreen,
) ??
Scaffold(
body: profileScreen,
),
);
},
),
];

View file

@ -5,14 +5,12 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
import 'package:flutter_chat_view/flutter_chat_view.dart';
@immutable
class CommunityChatUserStoryConfiguration {
const CommunityChatUserStoryConfiguration({
required this.userService,
required this.messageService,
required this.service,
class ChatUserStoryConfiguration {
const ChatUserStoryConfiguration({
required this.chatService,
required this.chatOptionsBuilder,
this.pageSize = 10,
this.onPressStartChat,
@ -31,10 +29,9 @@ class CommunityChatUserStoryConfiguration {
this.onPressChatTitle,
this.afterMessageSent,
this.messagePageSize = 20,
this.onPressUserProfile,
});
final ChatService service;
final ChatUserService userService;
final MessageService messageService;
final ChatService chatService;
final Function(BuildContext, ChatModel)? onPressChat;
final Function(BuildContext, ChatModel)? onDeleteChat;
final ChatTranslations translations;
@ -43,7 +40,7 @@ class CommunityChatUserStoryConfiguration {
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(ChatModel chat)? afterMessageSent;
final Function(String chatId)? afterMessageSent;
final Future<void> Function(ChatModel chat)? onReadChat;
final Function(ChatUserModel)? onPressCreateChat;
final ChatOptions Function(BuildContext context) chatOptionsBuilder;
@ -58,4 +55,5 @@ class CommunityChatUserStoryConfiguration {
final Color? iconColor;
final Widget Function(BuildContext context, Widget child)? chatPageBuilder;
final Function()? onPressStartChat;
final Function()? onPressUserProfile;
}

View file

@ -2,9 +2,12 @@
//
// SPDX-License-Identifier: BSD-3-Clause
mixin CommunityChatUserStoryRoutes {
mixin ChatUserStoryRoutes {
static const String chatScreen = '/chat';
static String chatDetailViewPath(String chatId) => '/chat-detail/$chatId';
static const String chatDetailScreen = '/chat-detail/:id';
static const String newChatScreen = '/new-chat';
static String chatProfileScreenPath(String chatId, String? userId) =>
'/chat-profile/$chatId/$userId';
static const String chatProfileScreen = '/chat-profile/:id/:userId';
}

View file

@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
name: flutter_community_chat
name: flutter_chat
description: A new Flutter package project.
version: 1.0.0
@ -16,15 +16,15 @@ dependencies:
flutter:
sdk: flutter
go_router: any
flutter_community_chat_view:
flutter_chat_view:
git:
url: https://github.com/Iconica-Development/flutter_community_chat
path: packages/flutter_community_chat_view
url: https://github.com/Iconica-Development/flutter_chat
path: packages/flutter_chat_view
ref: 1.0.0
flutter_community_chat_interface:
flutter_chat_interface:
git:
url: https://github.com/Iconica-Development/flutter_community_chat
path: packages/flutter_community_chat_interface
url: https://github.com/Iconica-Development/flutter_chat
path: packages/flutter_chat_interface
ref: 1.0.0
dev_dependencies:

View file

@ -4,7 +4,7 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_community_chat_firebase/dto/firebase_message_document.dart';
import 'package:flutter_chat_firebase/dto/firebase_message_document.dart';
@immutable
class FirebaseChatDocument {

View file

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

View file

@ -8,12 +8,14 @@ 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_community_chat_firebase/config/firebase_chat_options.dart';
import 'package:flutter_community_chat_firebase/dto/firebase_message_document.dart';
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.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';
class FirebaseMessageService with ChangeNotifier implements MessageService {
class FirebaseChatDetailService
with ChangeNotifier
implements ChatDetailService {
late final FirebaseFirestore _db;
late final FirebaseStorage _storage;
late final ChatUserService _userService;
@ -23,11 +25,11 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
StreamSubscription<QuerySnapshot>? _subscription;
DocumentSnapshot<Object>? lastMessage;
List<ChatMessageModel> _cumulativeMessages = [];
ChatModel? lastChat;
String? lastChat;
int? chatPageSize;
DateTime timestampToFilter = DateTime.now();
FirebaseMessageService({
FirebaseChatDetailService({
required ChatUserService userService,
FirebaseApp? app,
FirebaseChatOptions? options,
@ -40,10 +42,10 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
_options = options ?? const FirebaseChatOptions();
}
Future<void> _sendMessage(ChatModel chat, Map<String, dynamic> data) async {
Future<void> _sendMessage(String chatId, Map<String, dynamic> data) async {
var currentUser = await _userService.getCurrentUser();
if (chat.id == null || currentUser == null) {
if (currentUser == null) {
return;
}
@ -57,7 +59,7 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
.collection(
_options.chatsCollectionName,
)
.doc(chat.id);
.doc(chatId);
var newMessage = await chatReference
.collection(
@ -78,21 +80,13 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
.collection(
_options.chatsMetaDataCollectionName,
)
.doc(chat.id);
.doc(chatId);
await metadataReference.update({
'last_used': DateTime.now(),
'last_message': message,
});
if (_controller != null) {
if (chat.id != null &&
_controller!.hasListener &&
(_subscription == null)) {
_subscription = _startListeningForMessages(chat);
}
}
// 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
@ -107,7 +101,7 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
)
.doc(userId)
.collection(_options.userChatsCollectionName)
.doc(chat.id);
.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())
@ -120,7 +114,7 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
} else {
await userReference.set({
'amount_unread_messages': 1,
});
}, SetOptions(merge: true));
}
}
}
@ -129,10 +123,10 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
@override
Future<void> sendTextMessage({
required String text,
required ChatModel chat,
required String chatId,
}) {
return _sendMessage(
chat,
chatId,
{
'text': text,
},
@ -141,21 +135,17 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
@override
Future<void> sendImageMessage({
required ChatModel chat,
required String chatId,
required Uint8List image,
}) async {
if (chat.id == null) {
return;
}
var ref = _storage
.ref('${_options.chatsCollectionName}/${chat.id}/${const Uuid().v4()}');
.ref('${_options.chatsCollectionName}/$chatId/${const Uuid().v4()}');
return ref.putData(image).then(
(_) => ref.getDownloadURL().then(
(url) {
_sendMessage(
chat,
chatId,
{
'image_url': url,
},
@ -165,52 +155,16 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
);
}
Query<FirebaseMessageDocument> _getMessagesQuery(ChatModel chat) {
if (lastChat == null) {
lastChat = chat;
} else if (lastChat?.id != chat.id) {
_cumulativeMessages = [];
lastChat = chat;
lastMessage = null;
}
var query = _db
.collection(_options.chatsCollectionName)
.doc(chat.id)
.collection(_options.messagesCollectionName)
.orderBy('timestamp', descending: true)
.limit(chatPageSize!);
if (lastMessage == null) {
return query.withConverter<FirebaseMessageDocument>(
fromFirestore: (snapshot, _) =>
FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id),
toFirestore: (user, _) => user.toJson(),
);
}
return query
.startAfterDocument(lastMessage!)
.withConverter<FirebaseMessageDocument>(
fromFirestore: (snapshot, _) =>
FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id),
toFirestore: (user, _) => user.toJson(),
);
}
@override
Stream<List<ChatMessageModel>> getMessagesStream(ChatModel chat) {
Stream<List<ChatMessageModel>> getMessagesStream(String chatId) {
timestampToFilter = DateTime.now();
var messages = <ChatMessageModel>[];
_controller = StreamController<List<ChatMessageModel>>(
onListen: () {
var messagesCollection = _db
.collection(_options.chatsCollectionName)
.doc(chat.id)
.doc(chatId)
.collection(_options.messagesCollectionName)
.withConverter<FirebaseMessageDocument>(
fromFirestore: (snapshot, _) => FirebaseMessageDocument.fromJson(
snapshot.data()!, snapshot.id),
toFirestore: (user, _) => user.toJson(),
);
var query = messagesCollection
.where(
'timestamp',
isGreaterThan: timestampToFilter,
@ -219,67 +173,45 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
fromFirestore: (snapshot, _) => FirebaseMessageDocument.fromJson(
snapshot.data()!, snapshot.id),
toFirestore: (user, _) => user.toJson(),
);
)
.snapshots();
var stream = query.snapshots();
// Subscribe to the stream and process the updates
_subscription = stream.listen((snapshot) async {
var messages = <ChatMessageModel>[];
for (var messageDoc in snapshot.docs) {
var messageData = messageDoc.data();
_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(
(messageData.timestamp).millisecondsSinceEpoch,
);
// Check if the message is already in the list to avoid duplicates
if (timestampToFilter.isBefore(timestamp)) {
if (!messages.any((message) {
var timestamp = DateTime.fromMillisecondsSinceEpoch(
(messageData.timestamp).millisecondsSinceEpoch,
);
return timestamp == message.timestamp;
})) {
var sender = await _userService.getUser(messageData.sender);
if (sender != null) {
var timestamp = DateTime.fromMillisecondsSinceEpoch(
(messageData.timestamp).millisecondsSinceEpoch,
(data.timestamp).millisecondsSinceEpoch,
);
if (timestamp.isBefore(timestampToFilter)) {
return;
}
messages.add(
messageData.imageUrl != null
data.imageUrl != null
? ChatImageMessageModel(
sender: sender,
imageUrl: messageData.imageUrl!,
sender: sender!,
imageUrl: data.imageUrl!,
timestamp: timestamp,
)
: ChatTextMessageModel(
sender: sender,
text: messageData.text!,
sender: sender!,
text: data.text!,
timestamp: timestamp,
),
);
timestampToFilter = DateTime.now();
}
}
}
}
// Add the filtered messages to the controller
_controller?.add(messages);
_cumulativeMessages = [
..._cumulativeMessages,
...messages,
];
// remove all double elements
List<ChatMessageModel> uniqueObjects =
_cumulativeMessages.toSet().toList();
_cumulativeMessages = uniqueObjects;
_cumulativeMessages
.sort((a, b) => a.timestamp.compareTo(b.timestamp));
notifyListeners();
timestampToFilter = DateTime.now();
});
},
onCancel: () {
@ -288,72 +220,25 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
debugPrint('Canceling messages stream');
},
);
return _controller!.stream;
}
StreamSubscription<QuerySnapshot> _startListeningForMessages(ChatModel chat) {
debugPrint('Start listening for messages in chat ${chat.id}');
var snapshots = _getMessagesQuery(chat).snapshots();
return snapshots.listen(
(snapshot) async {
List<ChatMessageModel> messages =
List<ChatMessageModel>.from(_cumulativeMessages);
if (snapshot.docs.isNotEmpty) {
lastMessage = snapshot.docs.last;
for (var messageDoc in snapshot.docs) {
var messageData = messageDoc.data();
// Check if the message is already in the list to avoid duplicates
if (!messages.any((message) {
var timestamp = DateTime.fromMillisecondsSinceEpoch(
(messageData.timestamp).millisecondsSinceEpoch,
);
return timestamp == message.timestamp;
})) {
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,
),
);
}
}
}
}
_cumulativeMessages = messages;
messages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
_controller?.add(messages);
notifyListeners();
},
);
@override
void stopListeningForMessages() {
_subscription?.cancel();
_subscription = null;
_controller?.close();
_controller = null;
}
@override
Future<void> fetchMoreMessage(int pageSize, ChatModel chat) async {
Future<void> fetchMoreMessage(int pageSize, String chatId) async {
if (lastChat == null) {
lastChat = chat;
} else if (lastChat?.id != chat.id) {
lastChat = chatId;
} else if (lastChat != chatId) {
_cumulativeMessages = [];
lastChat = chat;
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
@ -361,7 +246,7 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
QuerySnapshot<FirebaseMessageDocument>? messagesQuerySnapshot;
var query = _db
.collection(_options.chatsCollectionName)
.doc(chat.id)
.doc(chatId)
.collection(_options.messagesCollectionName)
.orderBy('timestamp', descending: true)
.limit(pageSize);
@ -393,7 +278,6 @@ class FirebaseMessageService with ChangeNotifier implements MessageService {
List<FirebaseMessageDocument> 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) {

View file

@ -1,26 +1,24 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.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/dto/firebase_chat_document.dart';
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.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';
class FirebaseChatService implements ChatService {
class FirebaseChatOverviewService implements ChatOverviewService {
late FirebaseFirestore _db;
late FirebaseStorage _storage;
late ChatUserService _userService;
late FirebaseChatOptions _options;
DocumentSnapshot<Object?>? lastUserDocument;
String? lastGroupId;
List<String> chatIds = [];
int pageNumber = 1;
FirebaseChatService({
FirebaseChatOverviewService({
required ChatUserService userService,
FirebaseApp? app,
FirebaseChatOptions? options,
@ -33,266 +31,152 @@ class FirebaseChatService implements ChatService {
_options = options ?? const FirebaseChatOptions();
}
StreamSubscription<DocumentSnapshot> _addUnreadChatSubscription(
Future<int?> _addUnreadChatSubscription(
String chatId,
String userId,
Function(int) onUnreadChatsUpdated,
) {
var snapshots = _db
) async {
var snapshots = await _db
.collection(_options.usersCollectionName)
.doc(userId)
.collection(_options.userChatsCollectionName)
.doc(chatId)
.snapshots();
.get();
return snapshots.listen((snapshot) {
var data = snapshot.data();
onUnreadChatsUpdated(data?['amount_unread_messages'] ?? 0);
});
return snapshots.data()?['amount_unread_messages'];
}
StreamSubscription<QuerySnapshot> _addChatSubscription(
List<String> chatIds,
Function(List<ChatModel>) onReceivedChats,
) {
var snapshots = _db
@override
Stream<List<ChatModel>> getChatsStream() {
StreamSubscription? chatSubscription;
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),
fromFirestore: (snapshot, _) => FirebaseChatDocument.fromJson(
snapshot.data()!, snapshot.id),
toFirestore: (chat, _) => chat.toJson(),
)
.snapshots();
return snapshots.listen((snapshot) async {
var currentUser = await _userService.getCurrentUser();
var chats = <ChatModel>[];
for (var chatDoc in snapshot.docs) {
var chatData = chatDoc.data();
var messages = <ChatMessageModel>[];
if (chatData.lastMessage != null) {
var messageData = chatData.lastMessage!;
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,
),
);
}
}
List<ChatModel> chats = [];
ChatModel? chatModel;
if (chatData.personal) {
var otherUserId = List<String>.from(chatData.users).firstWhere(
chatSubscription = chatSnapshot.listen((event) async {
for (var element in event.docChanges) {
var chat = element.doc.data();
if (chat == null) return;
var otherUser = await _userService.getUser(
chat.users.firstWhere(
(element) => element != currentUser?.id,
);
var otherUser = await _userService.getUser(otherUserId);
if (otherUser != null) {
chatModel = PersonalChatModel(
id: chatDoc.id,
user: otherUser,
lastMessage: messages.isNotEmpty ? messages.last : null,
messages: messages,
canBeDeleted: chatData.canBeDeleted,
lastUsed: chatData.lastUsed == null
? null
: DateTime.fromMillisecondsSinceEpoch(
chatData.lastUsed!.millisecondsSinceEpoch,
),
);
}
int? 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 {
// group chat
var users = <ChatUserModel>[];
for (var userId in chatData.users) {
for (var userId in chat.users) {
var user = await _userService.getUser(userId);
if (user != null) {
users.add(user);
}
}
chatModel = GroupChatModel(
id: chatDoc.id,
title: chatData.title ?? '',
imageUrl: chatData.imageUrl ?? '',
lastMessage: messages.isNotEmpty ? messages.last : null,
messages: messages,
id: chat.id,
title: chat.title ?? '',
imageUrl: chat.imageUrl ?? '',
unreadMessages: unread,
users: users,
canBeDeleted: chatData.canBeDeleted,
lastUsed: chatData.lastUsed == null
lastMessage: chat.lastMessage != 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,
),
),
canBeDeleted: chat.canBeDeleted,
lastUsed: chat.lastUsed == null
? null
: DateTime.fromMillisecondsSinceEpoch(
chatData.lastUsed!.millisecondsSinceEpoch,
chat.lastUsed!.millisecondsSinceEpoch,
),
);
}
if (chatModel != null) {
_addUnreadChatSubscription(chatModel.id ?? '', currentUser?.id ?? '',
(unreadMessages) {
// the chatmodel should be updated to reflect the amount of unread messages
if (chatModel is PersonalChatModel) {
chatModel = (chatModel as PersonalChatModel)
.copyWith(unreadMessages: unreadMessages);
} else if (chatModel is GroupChatModel) {
chatModel = (chatModel as GroupChatModel)
.copyWith(unreadMessages: unreadMessages);
}
chats = chats
.map((chat) => chat.id == chatModel?.id ? chatModel! : chat)
.toList();
onReceivedChats(chats);
});
chats.add(chatModel!);
}
Set<String> uniqueIds = <String>{};
List<ChatModel> uniqueChatModels = [];
for (ChatModel chatModel in chats) {
if (uniqueIds.add(chatModel.id!)) {
uniqueChatModels.add(chatModel);
}
onReceivedChats(chats);
});
}
List<List<String>> _splitChatIds({
required List<String> chatIds,
int chunkSize = 10,
}) {
var result = <List<String>>[];
var length = chatIds.length;
for (var i = 0; i < length; i += chunkSize) {
var lastIndex = i + chunkSize;
result.add(
chatIds.sublist(i, lastIndex > length ? length : lastIndex),
);
}
return result;
}
Stream<List<ChatModel>> _getSpecificChatsStream(List<String> chatIds) {
late StreamController<List<ChatModel>> controller;
List<StreamSubscription<QuerySnapshot>> subscriptions = [];
var splittedChatIds = _splitChatIds(chatIds: chatIds);
controller = StreamController<List<ChatModel>>(
onListen: () {
var chats = <int, List<ChatModel>>{};
for (var chatIdPair in splittedChatIds.asMap().entries) {
subscriptions.add(
_addChatSubscription(
chatIdPair.value,
(data) {
chats[chatIdPair.key] = data;
var mergedChats = <ChatModel>[];
mergedChats.addAll(
chats.values.expand((element) => element),
);
mergedChats.sort(
uniqueChatModels.sort(
(a, b) => (b.lastUsed ?? DateTime.now()).compareTo(
a.lastUsed ?? DateTime.now(),
),
);
controller.add(mergedChats);
},
),
);
}
},
onCancel: () {
for (var subscription in subscriptions) {
subscription.cancel();
}
},
);
return controller.stream;
}
@override
Stream<List<ChatModel>> getChatsStream(int pageSize) {
late StreamController<List<ChatModel>> controller;
StreamSubscription? chatsSubscription;
controller = StreamController(
onListen: () async {
QuerySnapshot<Map<String, dynamic>> userSnapshot;
List<String> userChatIds;
var currentUser = await _userService.getCurrentUser();
var userQuery = _db
.collection(_options.usersCollectionName)
.doc(currentUser?.id)
.collection(_options.userChatsCollectionName);
if (lastUserDocument == null) {
userSnapshot = await userQuery.limit(pageSize).get();
userChatIds = userSnapshot.docs.map((chat) => chat.id).toList();
} else {
userSnapshot = await userQuery
.limit(pageSize)
.startAfterDocument(lastUserDocument!)
.get();
userChatIds = userSnapshot.docs.map((chat) => chat.id).toList();
}
var userGroupChatIds = await _db
.collection(_options.usersCollectionName)
.doc(currentUser?.id)
.get()
.then((userCollection) =>
userCollection.data()?[_options.groupChatsCollectionName])
.then((groupChatLabels) => groupChatLabels?.cast<String>())
.then((groupChatIds) {
var startIndex = (pageNumber - 1) * pageSize;
var endIndex = startIndex + pageSize;
if (groupChatIds != null) {
if (startIndex >= groupChatIds.length) {
return [];
}
var groupIds = groupChatIds.sublist(
startIndex, endIndex.clamp(0, groupChatIds.length));
lastGroupId = groupIds.last;
return groupIds;
}
return [];
controller.add(uniqueChatModels);
});
if (userSnapshot.docs.isNotEmpty) {
lastUserDocument = userSnapshot.docs.last;
}
pageNumber++;
chatIds.addAll([...userChatIds, ...userGroupChatIds]);
var chatsStream = _getSpecificChatsStream(chatIds);
chatsSubscription = chatsStream.listen((event) {
controller.add(event);
});
},
onCancel: () {
chatsSubscription?.cancel();
chatSubscription?.cancel();
},
);
return controller.stream;
@ -338,7 +222,7 @@ class FirebaseChatService implements ChatService {
);
} else {
var groupChatCollection = await _db
.collection(_options.chatsCollectionName)
.collection(_options.chatsMetaDataCollectionName)
.doc(chatId)
.withConverter(
fromFirestore: (snapshot, _) =>
@ -442,11 +326,10 @@ class FirebaseChatService implements ChatService {
.doc(userId)
.collection(_options.userChatsCollectionName)
.doc(reference.id)
.set({'users': userIds});
.set({'users': userIds}, SetOptions(merge: true));
}
chat.id = reference.id;
chatIds.add(chat.id!);
} else if (chat is GroupChatModel) {
if (currentUser?.id == null) {
return chat;
@ -479,11 +362,10 @@ class FirebaseChatService implements ChatService {
.doc(userId)
.collection(_options.groupChatsCollectionName)
.doc(reference.id)
.set({'users': userIds});
.set({'users': userIds}, SetOptions(merge: true));
}
chat.id = reference.id;
chatIds.add(chat.id!);
} else {
throw Exception('Chat type not supported for firebase');
}

View file

@ -5,12 +5,12 @@
import 'package:cloud_firestore/cloud_firestore.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/dto/firebase_user_document.dart';
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.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';
class FirebaseUserService implements ChatUserService {
FirebaseUserService({
class FirebaseChatUserService implements ChatUserService {
FirebaseChatUserService({
FirebaseApp? app,
FirebaseChatOptions? options,
}) {

View file

@ -0,0 +1,3 @@
export 'package:flutter_chat_firebase/service/firebase_chat_user_service.dart';
export 'package:flutter_chat_firebase/service/firebase_chat_detail_service.dart';
export 'package:flutter_chat_firebase/service/firebase_chat_overview_service.dart';

View file

@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
name: flutter_community_chat_firebase
name: flutter_chat_firebase
description: A new Flutter package project.
version: 1.0.0
publish_to: none
@ -19,10 +19,10 @@ dependencies:
firebase_storage: ^11.0.5
firebase_auth: ^4.1.2
uuid: ^4.0.0
flutter_community_chat_interface:
flutter_chat_interface:
git:
url: https://github.com/Iconica-Development/flutter_community_chat
path: packages/flutter_community_chat_interface
url: https://github.com/Iconica-Development/flutter_chat
path: packages/flutter_chat_interface
ref: 1.0.0
dev_dependencies:

View file

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

View file

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
import 'package:flutter_data_interface/flutter_data_interface.dart';
class ChatDataProvider extends DataInterface {
@ -14,6 +14,6 @@ class ChatDataProvider extends DataInterface {
static final Object _token = Object();
final ChatUserService userService;
final ChatService chatService;
final MessageService messageService;
final ChatOverviewService chatService;
final ChatDetailService messageService;
}

View file

@ -0,0 +1,39 @@
// 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 {
String? get id;
List<ChatMessageModel>? get messages;
int? get unreadMessages;
DateTime? get lastUsed;
ChatMessageModel? get lastMessage;
bool get canBeDeleted;
}
class ChatModel implements ChatModelInterface {
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;
}

View file

@ -0,0 +1,29 @@
// 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 ChatImageMessageModelInterface extends ChatMessageModel {
ChatImageMessageModelInterface({
required super.sender,
required super.timestamp,
});
String get imageUrl;
}
class ChatImageMessageModel implements ChatImageMessageModelInterface {
ChatImageMessageModel({
required this.sender,
required this.timestamp,
required this.imageUrl,
});
@override
final ChatUserModel sender;
@override
final DateTime timestamp;
@override
final String imageUrl;
}

View file

@ -0,0 +1,23 @@
// 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;
}
class ChatMessageModel implements ChatMessageModelInterface {
ChatMessageModel({
required this.sender,
required this.timestamp,
});
@override
final ChatUserModel sender;
@override
final DateTime timestamp;
}

View file

@ -0,0 +1,29 @@
// 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;
}
class ChatTextMessageModel implements ChatTextMessageModelInterface {
ChatTextMessageModel({
required this.sender,
required this.timestamp,
required this.text,
});
@override
final ChatUserModel sender;
@override
final DateTime timestamp;
@override
final String text;
}

View file

@ -1,8 +1,18 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
class ChatUserModel {
abstract class ChatUserModelInterface {
String? get id;
String? get firstName;
String? get lastName;
String? get imageUrl;
String? get fullName;
}
class ChatUserModel implements ChatUserModelInterface {
ChatUserModel({
this.id,
this.firstName,
@ -10,11 +20,15 @@ class ChatUserModel {
this.imageUrl,
});
@override
final String? id;
@override
final String? firstName;
@override
final String? lastName;
@override
final String? imageUrl;
@override
String? get fullName {
var fullName = '';

View file

@ -0,0 +1,91 @@
// 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;
GroupChatModelInterface copyWith({
String? id,
List<ChatMessageModel>? messages,
int? unreadMessages,
DateTime? lastUsed,
ChatMessageModel? lastMessage,
String? title,
String? imageUrl,
List<ChatUserModel>? users,
bool? canBeDeleted,
});
}
class GroupChatModel implements GroupChatModelInterface {
GroupChatModel({
this.id,
this.messages,
this.unreadMessages,
this.lastUsed,
this.lastMessage,
required this.canBeDeleted,
required this.title,
required this.imageUrl,
required this.users,
});
@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
GroupChatModel copyWith({
String? id,
List<ChatMessageModel>? messages,
int? unreadMessages,
DateTime? lastUsed,
ChatMessageModel? lastMessage,
bool? canBeDeleted,
String? title,
String? imageUrl,
List<ChatUserModel>? users,
}) {
return 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,
);
}
}

View file

@ -0,0 +1,78 @@
// 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;
PersonalChatModel copyWith({
String? id,
List<ChatMessageModel>? messages,
int? unreadMessages,
DateTime? lastUsed,
ChatMessageModel? lastMessage,
ChatUserModel? user,
bool? canBeDeleted,
});
}
class PersonalChatModel implements PersonalChatModelInterface {
PersonalChatModel({
this.id,
this.messages = const [],
this.unreadMessages,
this.lastUsed,
this.lastMessage,
this.canBeDeleted = true,
required this.user,
});
@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,
}) {
return PersonalChatModel(
id: id ?? this.id,
messages: messages ?? this.messages,
unreadMessages: unreadMessages ?? this.unreadMessages,
lastUsed: lastUsed ?? this.lastUsed,
lastMessage: lastMessage ?? this.lastMessage,
user: user ?? this.user,
canBeDeleted: canBeDeleted ?? this.canBeDeleted,
);
}
}

View file

@ -1,23 +1,25 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
abstract class MessageService with ChangeNotifier {
abstract class ChatDetailService with ChangeNotifier {
Future<void> sendTextMessage({
required ChatModel chat,
required String chatId,
required String text,
});
Future<void> sendImageMessage({
required ChatModel chat,
required String chatId,
required Uint8List image,
});
Stream<List<ChatMessageModel>> getMessagesStream(
ChatModel chat,
String chatId,
);
Future<void> fetchMoreMessage(int pageSize, ChatModel chat);
Future<void> fetchMoreMessage(int pageSize, String chatId);
List<ChatMessageModel> getMessages();
void stopListeningForMessages();
}

View file

@ -1,7 +1,7 @@
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
abstract class ChatService {
Stream<List<ChatModel>> getChatsStream(int pageSize);
abstract class ChatOverviewService {
Stream<List<ChatModel>> getChatsStream();
Future<ChatModel> getChatByUser(ChatUserModel user);
Future<ChatModel> getChatById(String id);
Future<void> deleteChat(ChatModel chat);

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
import 'package:flutter_chat_interface/flutter_chat_interface.dart';
abstract class ChatUserService {
Future<ChatUserModel?> getUser(String id);

View file

@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
name: flutter_community_chat_interface
name: flutter_chat_interface
description: A new Flutter package project.
version: 1.0.0
publish_to: none

View file

@ -4,7 +4,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
import 'package:flutter_chat_view/flutter_chat_view.dart';
void main() {
runApp(const MaterialApp(home: MyStatefulWidget()));

View file

@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
name: flutter_community_chat_view_example
name: flutter_chat_view_example
description: A standard flutter package.
publish_to: "none" # Remove this line if you wish to publish to pub.dev
@ -14,7 +14,7 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_community_chat_view:
flutter_chat_view:
path: ..
dev_dependencies:

View file

@ -1,6 +1,6 @@
// This is a generated file; do not edit or check into version control.
FLUTTER_ROOT=/opt/homebrew/Caskroom/flutter/3.10.2/flutter
FLUTTER_APPLICATION_PATH=/Users/mikedoornenbal/Documents/iconica/flutter_community_chat/packages/flutter_community_chat_view
FLUTTER_APPLICATION_PATH=/Users/mikedoornenbal/Documents/iconica/flutter_chat/packages/flutter_chat_view
COCOAPODS_PARALLEL_CODE_SIGN=true
FLUTTER_TARGET=lib/main.dart
FLUTTER_BUILD_DIR=build

View file

@ -1,7 +1,7 @@
#!/bin/sh
# This is a generated file; do not edit or check into version control.
export "FLUTTER_ROOT=/opt/homebrew/Caskroom/flutter/3.10.2/flutter"
export "FLUTTER_APPLICATION_PATH=/Users/mikedoornenbal/Documents/iconica/flutter_community_chat/packages/flutter_community_chat_view"
export "FLUTTER_APPLICATION_PATH=/Users/mikedoornenbal/Documents/iconica/flutter_chat/packages/flutter_chat_view"
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
export "FLUTTER_TARGET=lib/main.dart"
export "FLUTTER_BUILD_DIR=build"

View file

@ -2,13 +2,15 @@
//
// SPDX-License-Identifier: BSD-3-Clause
library flutter_community_chat_view;
library flutter_chat_view;
export 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
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_translations.dart';
export 'src/screens/chat_detail_screen.dart';
export 'src/screens/chat_entry_widget.dart';
export 'src/screens/chat_profile_screen.dart';
export 'src/screens/chat_screen.dart';
export 'src/screens/new_chat_screen.dart';

View file

@ -3,7 +3,7 @@
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
import 'package:flutter_chat_view/flutter_chat_view.dart';
class ChatBottom extends StatefulWidget {
const ChatBottom({

View file

@ -4,9 +4,9 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.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/services/date_formatter.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({

View file

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

View file

@ -3,8 +3,8 @@
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.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_chat_view/flutter_chat_view.dart';
import 'package:flutter_chat_view/src/components/chat_image.dart';
import 'package:flutter_image_picker/flutter_image_picker.dart';
class ChatOptions {

View file

@ -22,6 +22,7 @@ class ChatTranslations {
this.noUsersFound = 'No users found',
this.anonymousUser = 'Anonymous user',
this.chatCantBeDeleted = 'This chat can\'t be deleted',
this.chatProfileUsers = 'Users:',
});
final String chatsTitle;
@ -40,6 +41,7 @@ class ChatTranslations {
final String deleteChatModalConfirm;
final String noUsersFound;
final String chatCantBeDeleted;
final String chatProfileUsers;
/// Shown when the user has no name
final String anonymousUser;

View file

@ -7,10 +7,10 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.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_detail_row.dart';
import 'package:flutter_community_chat_view/src/components/image_loading_snackbar.dart';
import 'package:flutter_chat_view/flutter_chat_view.dart';
import 'package:flutter_chat_view/src/components/chat_bottom.dart';
import 'package:flutter_chat_view/src/components/chat_detail_row.dart';
import 'package:flutter_chat_view/src/components/image_loading_snackbar.dart';
class ChatDetailScreen extends StatefulWidget {
const ChatDetailScreen({
@ -19,18 +19,16 @@ class ChatDetailScreen extends StatefulWidget {
required this.onUploadImage,
required this.onReadChat,
required this.service,
required this.chatUserService,
required this.messageService,
required this.pageSize,
required this.chatId,
this.translations = const ChatTranslations(),
this.chat,
this.onPressChatTitle,
this.iconColor,
this.showTime = false,
super.key,
});
final ChatModel? chat;
final String chatId;
/// The id of the current user that is viewing the chat.
@ -47,8 +45,6 @@ class ChatDetailScreen extends StatefulWidget {
final Color? iconColor;
final bool showTime;
final ChatService service;
final ChatUserService chatUserService;
final MessageService messageService;
final int pageSize;
@override
@ -60,36 +56,39 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
ChatUserModel? currentUser;
ScrollController controller = ScrollController();
bool showIndicator = false;
late MessageService messageSubscription;
late ChatDetailService messageSubscription;
Stream<List<ChatMessageModel>>? stream;
ChatMessageModel? previousMessage;
List<Widget> detailRows = [];
ChatModel? chat;
@override
void initState() {
super.initState();
messageSubscription = widget.messageService;
messageSubscription = widget.service.chatDetailService;
messageSubscription.addListener(onListen);
if (widget.chat != null) {
stream = widget.messageService.getMessagesStream(widget.chat!);
stream?.listen((event) {});
Future.delayed(Duration.zero, () async {
chat =
await widget.service.chatOverviewService.getChatById(widget.chatId);
if (detailRows.isEmpty) {
await widget.messageService
.fetchMoreMessage(widget.pageSize, widget.chat!);
await widget.service.chatDetailService.fetchMoreMessage(
widget.pageSize,
chat!.id!,
);
}
stream = widget.service.chatDetailService.getMessagesStream(chat!.id!);
stream?.listen((event) {});
WidgetsBinding.instance.addPostFrameCallback((_) async {
await widget.onReadChat(chat!);
});
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.chat != null) {
widget.onReadChat(widget.chat!);
}
});
}
void onListen() {
var chatMessages = [];
chatMessages = widget.messageService.getMessages();
chatMessages = widget.service.chatDetailService.getMessages();
detailRows = [];
previousMessage = null;
for (var message in chatMessages) {
@ -106,8 +105,11 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
}
detailRows = detailRows.reversed.toList();
widget.onReadChat(widget.chat!);
if (mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await widget.onReadChat(chat!);
});
setState(() {});
}
}
@ -115,6 +117,7 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
@override
void dispose() {
messageSubscription.removeListener(onListen);
widget.service.chatDetailService.stopListeningForMessages();
super.dispose();
}
@ -132,18 +135,19 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
var messenger = ScaffoldMessenger.of(context)
..showSnackBar(
getImageLoadingSnackbar(widget.translations),
);
)
..activate();
if (image != null) {
await widget.onUploadImage(image);
}
Future.delayed(const Duration(seconds: 1), () {
messenger.hideCurrentSnackBar();
});
},
);
return FutureBuilder<ChatModel>(
future: widget.service.getChatById(widget.chat?.id ?? ''),
future: widget.service.chatOverviewService.getChatById(widget.chatId),
builder: (context, AsyncSnapshot<ChatModel> snapshot) {
var chatModel = snapshot.data;
return Scaffold(
@ -153,7 +157,7 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
onTap: () => widget.onPressChatTitle?.call(context, chatModel!),
child: Row(
mainAxisSize: MainAxisSize.min,
children: widget.chat == null
children: chat == null
? []
: [
if (chatModel is GroupChatModel) ...[
@ -202,8 +206,8 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
setState(() {
showIndicator = true;
});
await widget.messageService
.fetchMoreMessage(widget.pageSize, widget.chat!);
await widget.service.chatDetailService
.fetchMoreMessage(widget.pageSize, widget.chatId);
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
setState(() {

View file

@ -0,0 +1,146 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_chat_view/flutter_chat_view.dart';
class ChatEntryWidget extends StatefulWidget {
const ChatEntryWidget({
required this.chatService,
required this.onTap,
this.widgetSize = 75,
this.backgroundColor = Colors.grey,
this.icon = Icons.chat,
this.iconColor = Colors.black,
this.counterBackgroundColor = Colors.red,
this.textStyle,
super.key,
});
final ChatService chatService;
final Color backgroundColor;
final double widgetSize;
final Color counterBackgroundColor;
final Function() onTap;
final IconData icon;
final Color iconColor;
final TextStyle? textStyle;
@override
State<ChatEntryWidget> createState() => _ChatEntryWidgetState();
}
class _ChatEntryWidgetState extends State<ChatEntryWidget> {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => widget.onTap.call(),
child: StreamBuilder<int>(
stream:
widget.chatService.chatOverviewService.getUnreadChatsCountStream(),
builder: (BuildContext context, snapshot) {
return Stack(
alignment: Alignment.center,
children: [
Container(
width: widget.widgetSize,
height: widget.widgetSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.backgroundColor,
),
child: _AnimatedNotificationIcon(
icon: Icon(
widget.icon,
color: widget.iconColor,
size: widget.widgetSize / 1.5,
),
notifications: snapshot.data ?? 0,
),
),
Positioned(
right: 0.0,
top: 0.0,
child: Container(
width: widget.widgetSize / 2,
height: widget.widgetSize / 2,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.counterBackgroundColor,
),
child: Center(
child: Text(
'${snapshot.data ?? 0}',
style: widget.textStyle,
),
),
),
),
],
);
},
),
);
}
}
class _AnimatedNotificationIcon extends StatefulWidget {
const _AnimatedNotificationIcon({
required this.notifications,
required this.icon,
});
final int notifications;
final Icon icon;
@override
State<_AnimatedNotificationIcon> createState() =>
_AnimatedNotificationIconState();
}
class _AnimatedNotificationIconState extends State<_AnimatedNotificationIcon>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
@override
void initState() {
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
if (widget.notifications != 0) {
unawaited(_runAnimation());
}
super.initState();
}
@override
void dispose() {
super.dispose();
_animationController.dispose();
}
@override
void didUpdateWidget(covariant _AnimatedNotificationIcon oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.notifications != widget.notifications) {
_runAnimation();
}
}
Future<void> _runAnimation() async {
await _animationController.forward();
await _animationController.reverse();
}
@override
Widget build(BuildContext context) {
return RotationTransition(
turns: Tween(begin: 0.0, end: -.1)
.chain(CurveTween(curve: Curves.elasticIn))
.animate(_animationController),
child: widget.icon,
);
}
}

View file

@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:flutter_chat_view/flutter_chat_view.dart';
import 'package:flutter_chat_view/src/services/profile_service.dart';
import 'package:flutter_profile/flutter_profile.dart';
class ChatProfileScreen extends StatefulWidget {
const ChatProfileScreen({
required this.chatService,
required this.chatId,
required this.translations,
required this.onTapUser,
this.userId,
super.key,
});
final ChatTranslations translations;
final ChatService chatService;
final String chatId;
final String? userId;
final Function(String userId) onTapUser;
@override
State<ChatProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ChatProfileScreen> {
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
var hasUser = widget.userId == null;
return FutureBuilder<dynamic>(
future: hasUser
? widget.chatService.chatOverviewService.getChatById(widget.chatId)
: widget.chatService.chatUserService.getUser(widget.userId!),
builder: (context, snapshot) {
var data = snapshot.data;
User? user;
if (data is ChatUserModel) {
user = User(
firstName: data.firstName,
lastName: data.lastName,
imageUrl: data.imageUrl,
);
}
if (data is PersonalChatModel) {
user = User(
firstName: data.user.firstName,
lastName: data.user.lastName,
imageUrl: data.user.imageUrl,
);
} else if (data is GroupChatModel) {
user = User(
firstName: data.title,
imageUrl: data.imageUrl,
);
}
return Scaffold(
appBar: AppBar(
title: Text(
(data is ChatUserModel)
? '${data.firstName ?? ''} ${data.lastName ?? ''}'
: (data is PersonalChatModel)
? data.user.fullName ?? ''
: (data is GroupChatModel)
? data.title
: '',
),
),
body: snapshot.hasData
? ListView(
children: [
const SizedBox(
height: 10,
),
SizedBox(
height: 200,
width: size.width,
child: ProfilePage(
user: user!,
service: ChatProfileService(),
),
),
if (data is GroupChatModel) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 100),
child: Text(
widget.translations.chatProfileUsers,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
...data.users.map((e) {
var user = User(
firstName: e.firstName ?? '',
lastName: e.lastName ?? '',
imageUrl: e.imageUrl,
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: GestureDetector(
onTap: () => widget.onTapUser.call(e.id!),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Avatar(
user: user,
),
Text(
user.firstName!,
),
],
),
),
);
}),
],
],
)
: const Center(child: CircularProgressIndicator()),
);
},
);
}
}

View file

@ -0,0 +1,312 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
// ignore_for_file: lines_longer_than_80_chars
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_chat_view/flutter_chat_view.dart';
import 'package:flutter_chat_view/src/services/date_formatter.dart';
class ChatScreen extends StatefulWidget {
const ChatScreen({
required this.options,
required this.onPressStartChat,
required this.onPressChat,
required this.onDeleteChat,
required this.service,
this.onNoChats,
this.deleteChatDialog,
this.translations = const ChatTranslations(),
this.disableDismissForPermanentChats = false,
super.key,
});
final ChatOptions options;
final ChatTranslations translations;
final ChatService service;
final Function()? onPressStartChat;
final Function()? onNoChats;
final void Function(ChatModel chat) onDeleteChat;
final void Function(ChatModel chat) onPressChat;
/// Disable the swipe to dismiss feature for chats that are not deletable
final bool disableDismissForPermanentChats;
/// Method to optionally change the bottomsheetdialog
final Future<bool?> Function(BuildContext, ChatModel)? deleteChatDialog;
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final DateFormatter _dateFormatter = DateFormatter();
bool _hasCalledOnNoChats = false;
ScrollController controller = ScrollController();
bool showIndicator = false;
Stream<List<ChatModel>>? chats;
List<String> deletedChats = [];
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
var translations = widget.translations;
return widget.options.scaffoldBuilder(
AppBar(
title: Text(translations.chatsTitle),
centerTitle: true,
actions: [
StreamBuilder<int>(
stream:
widget.service.chatOverviewService.getUnreadChatsCountStream(),
builder: (BuildContext context, snapshot) => Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.only(right: 22.0),
child: Text(
'${snapshot.data ?? 0} ${translations.chatsUnread}',
style: const TextStyle(
color: Color(0xFFBBBBBB),
fontSize: 14,
),
),
),
),
),
],
),
Column(
children: [
Expanded(
child: ListView(
controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.only(top: 15.0),
children: [
StreamBuilder<List<ChatModel>>(
stream: widget.service.chatOverviewService.getChatsStream(),
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)) {
if (widget.onNoChats != null && !_hasCalledOnNoChats) {
_hasCalledOnNoChats = true; // Set the flag to true
WidgetsBinding.instance.addPostFrameCallback((_) async {
await widget.onNoChats!.call();
});
}
} else {
_hasCalledOnNoChats =
false; // Reset the flag if there are chats
}
return Column(
children: [
for (ChatModel chat in (snapshot.data ?? []).where(
(chat) => !deletedChats.contains(chat.id),
)) ...[
Builder(
builder: (context) => !(widget
.disableDismissForPermanentChats &&
!chat.canBeDeleted)
? Dismissible(
confirmDismiss: (_) async =>
widget.deleteChatDialog
?.call(context, chat) ??
showModalBottomSheet(
context: context,
builder: (BuildContext context) =>
Container(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
chat.canBeDeleted
? translations
.deleteChatModalTitle
: translations
.chatCantBeDeleted,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
if (chat.canBeDeleted)
Text(
translations
.deleteChatModalDescription,
style: const TextStyle(
fontSize: 16,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
TextButton(
child: Text(
translations
.deleteChatModalCancel,
style: const TextStyle(
fontSize: 16,
),
),
onPressed: () =>
Navigator.of(
context,
).pop(false),
),
if (chat.canBeDeleted)
ElevatedButton(
onPressed: () =>
Navigator.of(
context,
).pop(true),
child: Text(
translations
.deleteChatModalConfirm,
style:
const TextStyle(
fontSize: 16,
),
),
),
],
),
],
),
),
),
onDismissed: (_) {
setState(() {
deletedChats.add(chat.id!);
});
widget.onDeleteChat(chat);
},
background: Container(
color: Colors.red,
child: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
translations.deleteChatButton,
),
),
),
),
key: ValueKey(
chat.id.toString(),
),
child: ChatListItem(
widget: widget,
chat: chat,
translations: translations,
dateFormatter: _dateFormatter,
),
)
: ChatListItem(
widget: widget,
chat: chat,
translations: translations,
dateFormatter: _dateFormatter,
),
),
],
],
);
},
),
],
),
),
if (widget.onPressStartChat != null)
widget.options.newChatButtonBuilder(
context,
() async {
await widget.onPressStartChat!.call();
},
translations,
),
],
),
);
}
}
class ChatListItem extends StatelessWidget {
const ChatListItem({
required this.widget,
required this.chat,
required this.translations,
required DateFormatter dateFormatter,
super.key,
}) : _dateFormatter = dateFormatter;
final ChatScreen widget;
final ChatModel chat;
final ChatTranslations translations;
final DateFormatter _dateFormatter;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => widget.onPressChat(chat),
child: Container(
color: Colors.transparent,
child: widget.options.chatRowContainerBuilder(
(chat is PersonalChatModel)
? ChatRow(
unreadMessages: chat.unreadMessages ?? 0,
avatar: widget.options.userAvatarBuilder(
(chat as PersonalChatModel).user,
40.0,
),
title: (chat as PersonalChatModel).user.fullName ??
translations.anonymousUser,
subTitle: chat.lastMessage != null
? chat.lastMessage is ChatTextMessageModel
? (chat.lastMessage! as ChatTextMessageModel).text
: '📷 '
'${translations.image}'
: '',
lastUsed: chat.lastUsed != null
? _dateFormatter.format(
date: chat.lastUsed!,
)
: null,
)
: ChatRow(
title: (chat as GroupChatModel).title,
unreadMessages: chat.unreadMessages ?? 0,
subTitle: chat.lastMessage != null
? chat.lastMessage is ChatTextMessageModel
? (chat.lastMessage! as ChatTextMessageModel).text
: '📷 '
'${translations.image}'
: '',
avatar: widget.options.groupAvatarBuilder(
(chat as GroupChatModel).title,
(chat as GroupChatModel).imageUrl,
40.0,
),
lastUsed: chat.lastUsed != null
? _dateFormatter.format(
date: chat.lastUsed!,
)
: null,
),
),
),
);
}
}

View file

@ -3,14 +3,13 @@
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
import 'package:flutter_chat_view/flutter_chat_view.dart';
class NewChatScreen extends StatefulWidget {
const NewChatScreen({
required this.options,
required this.onPressCreateChat,
required this.service,
required this.userService,
this.translations = const ChatTranslations(),
super.key,
});
@ -18,7 +17,6 @@ class NewChatScreen extends StatefulWidget {
final ChatOptions options;
final ChatTranslations translations;
final ChatService service;
final ChatUserService userService;
final Function(ChatUserModel) onPressCreateChat;
@override
@ -40,7 +38,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
],
),
body: FutureBuilder<List<ChatUserModel>>(
future: widget.userService.getAllUsers(),
future: widget.service.chatUserService.getAllUsers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());

View file

@ -0,0 +1,24 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_profile/flutter_profile.dart';
class ChatProfileService extends ProfileService {
@override
FutureOr<void> editProfile(User user, String key, String? value) {
throw UnimplementedError();
}
@override
FutureOr<void> pageBottomAction() {
throw UnimplementedError();
}
@override
FutureOr<void> uploadImage(
BuildContext context, {
required Function(bool isUploading) onUploadStateChanged,
}) {
throw UnimplementedError();
}
}

View file

@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
name: flutter_community_chat_view
name: flutter_chat_view
description: A standard flutter package.
version: 1.0.0
@ -16,16 +16,20 @@ dependencies:
flutter:
sdk: flutter
intl: any
flutter_community_chat_interface:
flutter_chat_interface:
git:
url: https://github.com/Iconica-Development/flutter_community_chat
path: packages/flutter_community_chat_interface
url: https://github.com/Iconica-Development/flutter_chat
path: packages/flutter_chat_interface
ref: 1.0.0
cached_network_image: ^3.2.2
flutter_image_picker:
git:
url: https://github.com/Iconica-Development/flutter_image_picker
ref: 1.0.4
flutter_profile:
git:
ref: 1.1.5
url: https://github.com/Iconica-Development/flutter_profile
dev_dependencies:
flutter_test:

View file

@ -1,11 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
library flutter_community_chat;
export 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
export 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
export 'package:flutter_community_chat/src/routes.dart';
export 'package:flutter_community_chat/src/models/community_chat_configuration.dart';
export 'package:flutter_community_chat/src/flutter_community_chat_userstory.dart';

View file

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

View file

@ -1,3 +0,0 @@
export 'package:flutter_community_chat_firebase/service/firebase_user_service.dart';
export 'package:flutter_community_chat_firebase/service/firebase_message_service.dart';
export 'package:flutter_community_chat_firebase/service/firebase_chat_service.dart';

View file

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

View file

@ -1,23 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter_community_chat_interface/src/model/chat_message.dart';
abstract class ChatModel {
ChatModel({
this.id,
this.messages = const [],
this.unreadMessages,
this.lastUsed,
this.lastMessage,
this.canBeDeleted = true,
});
String? id;
List<ChatMessageModel>? messages;
int? unreadMessages;
DateTime? lastUsed;
ChatMessageModel? lastMessage;
bool canBeDeleted;
}

View file

@ -1,15 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter_community_chat_interface/src/model/chat_message.dart';
class ChatImageMessageModel extends ChatMessageModel {
ChatImageMessageModel({
required super.sender,
required super.timestamp,
required this.imageUrl,
});
final String imageUrl;
}

View file

@ -1,15 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter_community_chat_interface/src/model/chat_user.dart';
abstract class ChatMessageModel {
const ChatMessageModel({
required this.sender,
required this.timestamp,
});
final ChatUserModel sender;
final DateTime timestamp;
}

View file

@ -1,15 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter_community_chat_interface/src/model/chat_message.dart';
class ChatTextMessageModel extends ChatMessageModel {
ChatTextMessageModel({
required super.sender,
required super.timestamp,
required this.text,
});
final String text;
}

View file

@ -1,46 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
class GroupChatModel extends ChatModel {
GroupChatModel({
required this.title,
required this.imageUrl,
required this.users,
super.id,
super.messages,
super.lastUsed,
super.lastMessage,
super.unreadMessages,
super.canBeDeleted,
});
final String title;
final String imageUrl;
final List<ChatUserModel> users;
GroupChatModel copyWith({
String? id,
List<ChatMessageModel>? messages,
int? unreadMessages,
DateTime? lastUsed,
ChatMessageModel? lastMessage,
String? title,
String? imageUrl,
List<ChatUserModel>? users,
bool? canBeDeleted,
}) =>
GroupChatModel(
id: id ?? this.id,
messages: messages ?? this.messages,
unreadMessages: unreadMessages ?? this.unreadMessages,
lastUsed: lastUsed ?? this.lastUsed,
lastMessage: lastMessage ?? this.lastMessage,
title: title ?? this.title,
imageUrl: imageUrl ?? this.imageUrl,
users: users ?? this.users,
canBeDeleted: canBeDeleted ?? this.canBeDeleted,
);
}

View file

@ -1,38 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
class PersonalChatModel extends ChatModel {
PersonalChatModel({
required this.user,
super.id,
super.messages,
super.unreadMessages,
super.lastUsed,
super.lastMessage,
super.canBeDeleted,
});
final ChatUserModel user;
PersonalChatModel copyWith({
String? id,
List<ChatMessageModel>? messages,
int? unreadMessages,
DateTime? lastUsed,
ChatMessageModel? lastMessage,
ChatUserModel? user,
bool? canBeDeleted,
}) =>
PersonalChatModel(
id: id ?? this.id,
messages: messages ?? this.messages,
unreadMessages: unreadMessages ?? this.unreadMessages,
lastUsed: lastUsed ?? this.lastUsed,
lastMessage: lastMessage ?? this.lastMessage,
user: user ?? this.user,
canBeDeleted: canBeDeleted ?? this.canBeDeleted,
);
}

View file

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

View file

@ -1,367 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
// ignore_for_file: lines_longer_than_80_chars
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
import 'package:flutter_community_chat_view/src/services/date_formatter.dart';
class ChatScreen extends StatefulWidget {
const ChatScreen({
required this.options,
required this.onPressStartChat,
required this.onPressChat,
required this.onDeleteChat,
required this.service,
required this.pageSize,
this.onNoChats,
this.deleteChatDialog,
this.translations = const ChatTranslations(),
this.disableDismissForPermanentChats = false,
super.key,
});
final ChatOptions options;
final ChatTranslations translations;
final ChatService service;
final Function()? onPressStartChat;
final Function()? onNoChats;
final void Function(ChatModel chat) onDeleteChat;
final void Function(ChatModel chat) onPressChat;
final int pageSize;
/// Disable the swipe to dismiss feature for chats that are not deletable
final bool disableDismissForPermanentChats;
/// Method to optionally change the bottomsheetdialog
final Future<bool?> Function(BuildContext, ChatModel)? deleteChatDialog;
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final DateFormatter _dateFormatter = DateFormatter();
bool _hasCalledOnNoChats = false;
ScrollController controller = ScrollController();
bool showIndicator = false;
Stream<List<ChatModel>>? chats;
List<String> deletedChats = [];
@override
void initState() {
getChats();
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
void getChats() {
setState(() {
chats = widget.service.getChatsStream(widget.pageSize);
});
}
@override
Widget build(BuildContext context) {
var translations = widget.translations;
return widget.options.scaffoldBuilder(
AppBar(
title: Text(translations.chatsTitle),
centerTitle: true,
actions: [
StreamBuilder<int>(
stream: widget.service.getUnreadChatsCountStream(),
builder: (BuildContext context, snapshot) => Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.only(right: 22.0),
child: Text(
'${snapshot.data ?? 0} ${translations.chatsUnread}',
style: const TextStyle(
color: Color(0xFFBBBBBB),
fontSize: 14,
),
),
),
),
),
],
),
Column(
children: [
Expanded(
child: Listener(
onPointerMove: (event) {
var isTop = controller.position.pixels ==
controller.position.maxScrollExtent;
if (showIndicator == false &&
!isTop &&
controller.position.userScrollDirection ==
ScrollDirection.reverse) {
setState(() {
showIndicator = true;
});
getChats();
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
setState(() {
showIndicator = false;
});
}
});
}
},
child: ListView(
controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.only(top: 15.0),
children: [
StreamBuilder<List<ChatModel>>(
stream: chats,
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)) {
if (widget.onNoChats != null && !_hasCalledOnNoChats) {
_hasCalledOnNoChats = true; // Set the flag to true
WidgetsBinding.instance
.addPostFrameCallback((_) async {
await widget.onNoChats!.call();
getChats();
});
}
} else {
_hasCalledOnNoChats =
false; // Reset the flag if there are chats
}
return Column(
children: [
for (ChatModel chat in (snapshot.data ?? []).where(
(chat) => !deletedChats.contains(chat.id),
)) ...[
Builder(
builder: (context) => !(widget
.disableDismissForPermanentChats &&
!chat.canBeDeleted)
? Dismissible(
confirmDismiss: (_) async =>
widget.deleteChatDialog
?.call(context, chat) ??
showModalBottomSheet(
context: context,
builder: (BuildContext context) =>
Container(
padding:
const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
chat.canBeDeleted
? translations
.deleteChatModalTitle
: translations
.chatCantBeDeleted,
style: const TextStyle(
fontSize: 20,
fontWeight:
FontWeight.bold,
),
),
const SizedBox(height: 16),
if (chat.canBeDeleted)
Text(
translations
.deleteChatModalDescription,
style: const TextStyle(
fontSize: 16,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment:
MainAxisAlignment
.center,
children: [
TextButton(
child: Text(
translations
.deleteChatModalCancel,
style:
const TextStyle(
fontSize: 16,
),
),
onPressed: () =>
Navigator.of(
context,
).pop(false),
),
if (chat.canBeDeleted)
ElevatedButton(
onPressed: () =>
Navigator.of(
context,
).pop(true),
child: Text(
translations
.deleteChatModalConfirm,
style:
const TextStyle(
fontSize: 16,
),
),
),
],
),
],
),
),
),
onDismissed: (_) {
setState(() {
deletedChats.add(chat.id!);
});
widget.onDeleteChat(chat);
},
background: Container(
color: Colors.red,
child: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
translations.deleteChatButton,
),
),
),
),
key: ValueKey(
chat.id.toString(),
),
child: ChatListItem(
widget: widget,
chat: chat,
translations: translations,
dateFormatter: _dateFormatter,
),
)
: ChatListItem(
widget: widget,
chat: chat,
translations: translations,
dateFormatter: _dateFormatter,
),
),
],
if (showIndicator &&
snapshot.connectionState !=
ConnectionState.done) ...[
const SizedBox(
height: 10,
),
const CircularProgressIndicator(),
const SizedBox(
height: 10,
),
],
],
);
},
),
],
),
),
),
if (widget.onPressStartChat != null)
widget.options.newChatButtonBuilder(
context,
() async {
await widget.onPressStartChat!.call();
getChats();
},
translations,
),
],
),
);
}
}
class ChatListItem extends StatelessWidget {
const ChatListItem({
required this.widget,
required this.chat,
required this.translations,
required DateFormatter dateFormatter,
super.key,
}) : _dateFormatter = dateFormatter;
final ChatScreen widget;
final ChatModel chat;
final ChatTranslations translations;
final DateFormatter _dateFormatter;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => widget.onPressChat(chat),
child: Container(
color: Colors.transparent,
child: widget.options.chatRowContainerBuilder(
(chat is PersonalChatModel)
? ChatRow(
unreadMessages: chat.unreadMessages ?? 0,
avatar: widget.options.userAvatarBuilder(
(chat as PersonalChatModel).user,
40.0,
),
title: (chat as PersonalChatModel).user.fullName ??
translations.anonymousUser,
subTitle: chat.lastMessage != null
? chat.lastMessage is ChatTextMessageModel
? (chat.lastMessage! as ChatTextMessageModel).text
: '📷 '
'${translations.image}'
: '',
lastUsed: chat.lastUsed != null
? _dateFormatter.format(
date: chat.lastUsed!,
)
: null,
)
: ChatRow(
title: (chat as GroupChatModel).title,
unreadMessages: chat.unreadMessages ?? 0,
subTitle: chat.lastMessage != null
? chat.lastMessage is ChatTextMessageModel
? (chat.lastMessage! as ChatTextMessageModel).text
: '📷 '
'${translations.image}'
: '',
avatar: widget.options.groupAvatarBuilder(
(chat as GroupChatModel).title,
(chat as GroupChatModel).imageUrl,
40.0,
),
lastUsed: chat.lastUsed != null
? _dateFormatter.format(
date: chat.lastUsed!,
)
: null,
),
),
),
);
}
}

View file

@ -1,6 +1,6 @@
name: flutter_community_chat_workspace
name: flutter_chat_workspace
environment:
sdk: '>=3.1.0 <4.0.0'
sdk: ">=3.1.0 <4.0.0"
dev_dependencies:
melos: ^3.0.1