Compare commits

..

No commits in common. "master" and "0.2.7" have entirely different histories.

220 changed files with 7081 additions and 8827 deletions

3
.fvmrc
View file

@ -1,3 +0,0 @@
{
"flutter": "3.24.3"
}

View file

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

View file

@ -1,14 +0,0 @@
name: Iconica Standard Component Documentation Workflow
# Workflow Caller version: 1.0.0
on:
release:
types: [published]
workflow_dispatch:
jobs:
call-iconica-component-documentation-workflow:
uses: Iconica-Development/.github/.github/workflows/component-documentation.yml@master
secrets: inherit
permissions: write-all

View file

@ -1,15 +0,0 @@
name: Iconica Standard Melos CI Workflow
# Workflow Caller version: 1.0.0
on:
pull_request:
workflow_dispatch:
jobs:
call-global-iconica-workflow:
uses: Iconica-Development/.github/.github/workflows/melos-ci.yml@master
secrets: inherit
permissions: write-all
with:
subfolder: '.' # add optional subfolder to run workflow in
flutter_version: 3.24.3

View file

@ -1,15 +0,0 @@
name: Iconica Standard Component Release Workflow
# Workflow Caller version: 1.0.0
on:
release:
types: [published]
workflow_dispatch:
jobs:
call-global-iconica-workflow:
uses: Iconica-Development/.github/.github/workflows/component-release.yml@master
secrets: inherit
permissions: write-all

23
.gitignore vendored
View file

@ -19,7 +19,6 @@ migrate_working_dir/
*.ipr
*.iws
.idea/
ios
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
@ -28,28 +27,10 @@ ios
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
# /pubspec.lock
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/
.flutter-plugins-dependencies
.flutter-plugins
.metadata
pubspec.lock
packages/flutter_chat/pubspec.lock
packages/firebase_chat_repository/pubspec.lock
packages/chat_repository_interface/pubspec.lock
android
linux
macos
web
windows
pubspec_overrides.yaml
# FVM Version Cache
.fvm/
.fvmrc
.flutter-plugins

View file

@ -1,175 +1,3 @@
## 6.0.0
- Added pending message repository to temporarily store messages that are not yet received by the backend
- Added pending message icons next to time on default messages
- Added pending image uploading by base64encoding the data and putting it in the image url
- Added image pre-loading to handle error and loading states
- Added reload button in case of an image loading error
- Added messageStatus field to MessageModel to differentiate between sent and pending messages
## 5.1.2
- Added correct padding inbetween time indicators and names
- Show names if a new day occurs and an indicator is shown
## 5.1.1
- Expose default indicator builder from the indicator options
## 5.1.0
- Added optional time indicator in chat detail screens to show which day the message is posted
## 5.0.0
- Removed the default values for the ChatOptions that are now nullable so they resolve to the ThemeData values
- Added chatAlignment to change the alignment of the chat messages
- Added messageType to the ChatMessageModel to allow for different type of messages, it is nullable to remain backwards compatible
- Get the color for the imagepicker from the Theme's primaryColor
- Added chatMessageBuilder to the userstory configuration to customize the chat messages
- Update the default chat message builder to a new design
- Added ChatScope that can be used to get the ChatService and ChatTranslations from the context. If you use individual components instead of the userstory you need to wrap them with the ChatScope. The options and service will be removed from all the component constructors.
- Added getAllUsersForChat to UserRepositoryInterface for fetching all users for a chat
- Added flutter_hooks as a dependency for easier state management
- Added FlutterChatDetailNavigatorUserstory that can be used to start the userstory from the chat detail screen without having the chat overview screen
- Changed the ChatDetailScreen to use the chatId instead of the ChatModel, the screen will now fetch the chat from the ChatService
- Changed baseScreenBuilder to include a chatTitle that can be used to show provide the title logic to apps that use the baseScreenBuilder
- Added loadNewMessagesAfter, loadOldMessagesBefore and removed pagination from getMessages in the ChatRepositoryInterface to change pagination behavior to rely on the stream and two methods indicating that more messages should be added to the stream
- Added chatTitleResolver that can be used to resolve the chat title from the chat model or return null to allow for default behavior
- Added ChatPaginationControls to the ChatOptions to allow for more control over the pagination
- Fixed that chat message is automatically sent when the user presses enter on the keyboard in the chat input
- Added sender and chatId to uploadImage in the ChatRepositoryInterface
- Added imagePickerBuilder to the builders in the ChatOptions to override the image picker with a custom implementation that needs to return a Future<Uint8List?>
- Changed the ChatBottomInputSection to be multiline and go from 45px to 120px in height depending on how many lines are in the textfield
- Added chatScreenBuilder to the userstory configuration to customize the specific chat screen with a ChatModel as argument
- Added senderTitleResolver to the ChatOptions to resolve the title of the sender in the chat message
- Added imageProviderResolver to the ChatOptions to resolve ImageProvider for all images in the userstory
- Added enabled boolean to the messageInputBuilder and made parameters named
- Added autoScrollTriggerOffset to the ChatPaginationControls to adjust when the auto scroll should be enabled
- Added the ability to set the color of the CircularProgressIndicator of the ImageLoadingSnackbar by theme.snackBarTheme.actionTextColor
- Added semantics for variable text, buttons and textfields
- Added flag to enable/disable loading new and old messages on scrolling to the end of the current view.
- Updated description of packages to be more descriptive
## 4.0.0
- Move to the new user story architecture
## 3.1.0
- Fix center the texts for no users found with search and type first message
- Fix styling for the whole userstory
- Add groupchat profile picture, and bio to the groupchat creation screen
- Updated profile of users and groups
## 3.0.1
- fix bug where you could make multiple groups quickly by routing back to the previous screen
- fix bug where you would route back to the user selection screen insterad of routing back to the chat overview screen
- Add onPopInvoked callback to the userstory to add custom behaviour for the back button on the chatscreen
- Handle overflows for users with a long name.
- Remove the scaffold backgrounds because they should be inherited from the scaffold theme
## 3.0.0
- Add theming
- add validator for group name
- fix spamming buttons
- fix user list flickering on the group creation screen
## 2.0.0
- Add a serviceBuilder to the userstory configuration
- Add a translationsBuilder to the userstory configuration
- Change onPressUserProfile callback to use a ChatUserModel instead of a String
- Add a enableGroupChatCreation boolean to the userstory configuration to enable or disable group chat creation
- Change the ChatTranslations constructor to require all translations or use the ChatTranslations.empty constructor if you don't want to specify all translations
- Remove the Divider between the users on the new chat screen
- Add option to set a custom padding around the list of chats
- Fix nullpointer when firstWhere returns null because there is only 1 person in a groupchat
## 1.4.3
- Added default styling.
- Fixed groupchats using navigator
## 1.4.2
- Added doc comments
- Fixed bug when creating a group chat with the `LocalChatService`
- Updated readme
## 1.4.1
- Made UI changes to match the Figma design
## 1.4.0
- Add way to create group chats
- Update flutter_profile to 1.3.0
- Update flutter_image_picker to 1.0.5
## 1.3.1
- Added more options for styling the UI.
- Changed the way profile images are shown.
- Added an ontapUser in the chat.
- Changed the way the time is shown in the chat after a message.
- Added option to customize chat title and username chat message widget.
## 1.2.1
- Fixed bug in the LocalChatService
## 1.2.0
- Added linter and workflow
## 1.1.0
- Added LocalChatService for example app
## 1.0.0
- Added pagination for the ChatDetailScreen
- Added routes with Go_router and Navigator
- Added ChatEntryWidget
## 0.6.0 - December 1 2023
- Made the message controller nullable
- Improved chat UI and added showTime option for chatDetailScreen to always show the time
## 0.5.0 - November 29 2023
- Added the option to add your own dialog on chat delete and addded the option to make the chat not deletable
## 0.4.2 - November 24 2023
- Fix groupchats seen as personal chat when there are unread messages
## 0.4.1 - November 22 2023
- Add groupName for groupchat avatarbuilder
## 0.4.0 - November 6 2023
- Show amount of unread messages per chat
- More intuitive chat UI
- Fix default profile avatars
## 0.3.4 - October 25 2023
- Add interface methods for getting amount of unread messages
## 0.3.3 - October 10 2023
- Add icon color property for icon buttons
## 0.3.2 - September 26 2023
- Fix fullname getter for nullable values
## 0.3.1 - July 11 2023
- Removed image message when there is no last message in a chat
## 0.3.0 - March 31 2023
- Added support for group chats
## 0.0.1 - October 17th 2022
- Initial release

View file

@ -1,7 +0,0 @@
List of Features from this component:
* A chat overview screen
* A chat detail screen
* Chats can be between 2 users or group chats
* Chats can contain messages or images
* Interface for interacting with the messages(createChat, sendMessage, etc.)
* Firebase implementation of the interface

View file

@ -1,4 +1,4 @@
Copyright (c) 2024 Iconica, All rights reserved.
Copyright (c) 2022 Iconica, All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

272
README.md
View file

@ -1,270 +1,66 @@
# Flutter Chat
Flutter Chat is a package which gives the possibility to add a (personal or group) chat to your Flutter-application.
By default this package adds support for a Firebase back-end but you can also add a custom back-end (like a Websocket-API) by extending the `ChatInterface` interface from the `flutter_chat_interface` package.
![Flutter Chat GIF](example.gif)
The default UI is based on a Figma design that defines this component, however it's only accessible by Iconica developers:
[Figma design](https://www.figma.com/file/4WkjwynOz5wFeFBRqTHPeP/Iconica-Design-System?type=design&node-id=357%3A3342&mode=design&t=XulkAJNPQ32ARxWh-1).
There is also a Figma clickable prototype that demonstrates this component:
[Figma clickable prototype)[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]
# Flutter Community 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.
## 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:
```yaml
flutter_chat:
git:
url: https://github.com/Iconica-Development/flutter_chat
path: packages/flutter_chat
```
flutter_community_chat:
git:
url: https://github.com/Iconica-Development/flutter_community_chat.git
path: packages/flutter_community_chat
```
You can use the `LocalChatService` to test the package in your project:
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:
```dart
ChatUserStoryConfiguration(
chatService: LocalChatService(),
),
```
If you are going to use Firebase as the back-end of the Chat, you should also add the following package as a dependency to your `pubspec.yaml` file:
```yaml
flutter_chat_firebase:
git:
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.
Make sure you are authenticated using the `firebase_auth` package or adjust your firebase rules, otherwise you won't be able to retreive data.
Also make sure you have the corresponding collections in your Firebase project as defined in `FirebaseChatOptions`, you can override the
default paths as you wish:
```dart
const FirebaseChatOptions({
this.groupChatsCollectionName = 'group_chats',
this.chatsCollectionName = 'chats',
this.messagesCollectionName = 'messages',
this.usersCollectionName = 'users',
this.chatsMetaDataCollectionName = 'chat_metadata',
this.userChatsCollectionName = 'chats',
});
```
Also the structure of your data should be equal to our predefined models, you can implement any model by making your own model and implementing one of the predefined interfaces like so:
```dart
class ChatMessageModel implements ChatMessageModelInterface {
ChatMessageModel({
required this.sender,
required this.timestamp,
});
@override
final ChatUserModel sender;
@override
final DateTime timestamp;
}
```
The various interfaces you can implement:
- `ChatUserModelInterface`,
- `ChatImageMessageModelInterface`,
- `ChatTextMessageModelInterface`
- `ChatMessageModelInterface`,
- `ChatModelInterface`,
- `GroupChatModelInterface`,
- `PersonalChatModelInterface`,
Add the following to your project to use the camera or photo library to send pictures:
For ios add the following lines to your info.plist:
```plist
<key>NSCameraUsageDescription</key>
<string>Access camera</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Library</string>
```
For android add the following lines to your AndroidManifest.xml:
```xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.GALLERY"/>
flutter_community_chat_firebase:
git:
url: https://github.com/Iconica-Development/flutter_community_chat.git
path: packages/flutter_community_chat_firebase
```
## How to use
To use the module within your Flutter-application you should add the following code to the build-method of a chosen widget.
To use the module within your Flutter-application with predefined `go_router` routes you should add the following:
Add go_router as dependency to your project, then add the following configuration to your flutter_application:
```dart
List<GoRoute> getChatRoutes() => getChatStoryRoutes(
ChatUserStoryConfiguration(
chatService: chatService,
chatOptionsBuilder: (ctx) => const ChatOptions(),
),
);
```
CommunityChat(
dataProvider: FirebaseCommunityChatDataProvider(),
)
```
You can override any method in the `ChatUserStoryConfiguration`.
In this example we provide a `FirebaseCommunityChatDataProvider` as a data provider. You can also specify your own implementation here of the `CommunityChatInterface` interface.
Add the `getChatRoutes()` to your go_router routes like so:
You can also include your custom configuration for both the Community Chat itself as the Image Picker which is included in this package. You can specify those configurations as a parameter:
```dart
final GoRouter _router = GoRouter(
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const MyHomePage(
title: "home",
);
},
),
...getChatRoutes()
],
);
```
The routes that can be used to navigate are:
For routing to the `ChatScreen`:
```dart
static const String chatScreen = '/chat';
```
For routing to the `ChatDetailScreen`:
```dart
static String chatDetailViewPath(String chatId) => '/chat-detail/$chatId';
static const String chatDetailScreen = '/chat-detail/:id';
```
For routing to the `NewChatScreen`:
```dart
static const String newChatScreen = '/new-chat';
```
For routing to the `ChatProfileScreen`:
you can see the information about a person or group you started a chat with.
If the userId is null a group profile screen will be shown otherwise the profile of a single person will be shown.
```dart
static String chatProfileScreenPath(String chatId, String? userId) =>
'/chat-profile/$chatId/$userId';
static const String chatProfileScreen = '/chat-profile/:id/:userId';
```
Add the following code to the build-method of a chosen widget to use the module within your Flutter-application without predefined `go_router` routes but with Navigator routes:
```dart
chatNavigatorUserStory(
ChatUserStoryConfiguration(
chatService: ChatService,
chatOptionsBuilder: (ctx) => const ChatOptions(),
),
context,
);
```
Just like with the `go_router` routes you can override any methods in the `ChatUserStoryConfiguration`.
Or create your own routing using the screens.
Add the following code to add the `ChatScreen`:
```dart
ChatScreen(
options: options,
onPressStartChat: onPressStartChat,
onPressChat: onPressChat,
onDeleteChat: onDeleteChat,
service: service,
pageSize: pageSize,
);
```
The `ChatDetailScreen` shows the messages that are in the current chat you selected.
To add the `ChatDetailScreen` add the following code:
```dart
ChatDetailScreen(
options: options,
onMessageSubmit: onMessageSubmit,
onUploadImage: onUploadImage,
onReadChat: onReadChat,
service: service,
);
```
On the `NewChatScreen` you can select a person to chat.
Add the following coe to add the `NewChatScreen`:
```dart
NewChatScreen(
options: options,
onPressCreateChat: onPressCreateChat,
service: service,
);
```
On the `ChatProfileScreen` you can see the information about a person or group you started a chat with.
A group profile screen will be shown if the userId is null, otherwise the profile of a single person will be shown.
```dart
ChatProfileScreen(
chatService: chatservice,
chatId: chatId,
translations: translations,
onTapUser: onTapUser,
userId: userId,
);
```
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 an `onTap` to the `ChatEntryWidget` so it routes to the `ChatScreen`.
Add the following code to add the `ChatEntryWidget`:
```dart
ChatEntryWidget(
chatService: chatService,
onTap: onTap,
);
CommunityChat(
dataProvider: FirebaseCommunityChatDataProvider(),
imagePickerTheme: ImagePickerTheme(),
chatOptions: ChatOptions(),
)
```
The `ChatOptions` has its own parameters, as specified below:
| Parameter | Explanation |
|-----------|-------------|
| newChatButtonBuilder | Builds the 'New Chat' button, to initiate a new chat session. This button is displayed on the chat overview. |
| messageInputBuilder | Builds the text input which is displayed within the chat view, responsible for sending text messages. |
| chatRowContainerBuilder | Builds a chat row. A row with the users' avatar, name and eventually the last massage sended in the chat. This builder is used both in the _chat overview screen_ as in the _new chat screen_. |
| imagePickerContainerBuilder | Builds the container around the ImagePicker. |
| newChatButtonBuilder | Builds the 'New Chat' button, to initiate a new chat session. This button is displayed on the chat overview. |
| messageInputBuilder | Builds the text input which is displayed within the chat view, responsible for sending text messages. |
| chatRowContainerBuilder | Builds a chat row. A row with the users' avatar, name and eventually the last massage sended in the chat. This builder is used both in the *chat overview screen* as in the *new chat screen*. |
| imagePickerContainerBuilder | Builds the container around the ImagePicker. |
| closeImagePickerButtonBuilder | Builds the close button for the Image Picker pop-up window. |
| scaffoldBuilder | Builds the default Scaffold-widget around the Community Chat. The chat title is displayed within the Scaffolds' title for example. |
| scaffoldBuilder | Builds the default Scaffold-widget around the Community Chat. The chat title is displayed within the Scaffolds' title for example. |
The `ImagePickerTheme` also has its own parameters, how to use these parameters can be found in [the documentation of the flutter_image_picker package](https://github.com/Iconica-Development/flutter_image_picker).
The `ImagePickerTheme` also has its own parameters, how to use these parameters can be found in [the documentation of the flutter_image_picker package](https://github.com/Iconica-Development/flutter_image_picker).
## Issues
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).
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).
## 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_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_community_chat/pulls).
## Author
This `flutter_chat` for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>
This `flutter_community_chat` for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 KiB

View file

@ -1,4 +1,4 @@
name: flutter_chat
name: flutter_community_chat
packages:
- packages/**
@ -9,31 +9,25 @@ command:
scripts:
lint:all:
run: dart run melos run analyze && dart run melos run format-check
run: melos run analyze && melos run format
description: Run all static analysis checks.
get:
run: |
melos exec -c 1 -- "flutter pub get"
melos exec --scope="*example*" -c 1 -- "flutter pub get"
run: melos exec -c 1 -- "flutter pub get"
upgrade:
run: melos exec -c 1 -- "flutter pub upgrade"
create:
# run create in the example folder of flutter_chat_view
run: melos exec --scope="*example*" -c 1 -- "flutter create ."
analyze:
run: |
dart run melos exec -c 1 -- \
melos exec -c 1 -- \
flutter analyze --fatal-infos
description: Run `flutter analyze` for all packages.
format:
run: dart run melos exec dart format .
description: Run `dart format` for all packages.
run: melos exec flutter format . --fix
description: Run `flutter format` for all packages.
format-check:
run: dart run melos exec dart format . --set-exit-if-changed
description: Run `dart format` checks for all packages.
run: melos exec flutter format . --set-exit-if-changed
description: Run `flutter format` checks for all packages.

View file

@ -1,29 +0,0 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
build/

View file

@ -1 +0,0 @@
../../CHANGELOG.md

View file

@ -1 +0,0 @@
../../LICENSE

View file

@ -1,9 +0,0 @@
include: package:flutter_iconica_analysis/components_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -1,16 +0,0 @@
// exceptions
export "src/exceptions/chat.dart";
// Interfaces
export "src/interfaces/chat_repostory_interface.dart";
export "src/interfaces/pending_message_repository_interface.dart";
export "src/interfaces/user_repository_interface.dart";
// Local implementations
export "src/local/local_chat_repository.dart";
export "src/local/local_pending_message_repository.dart";
export "src/local/local_user_repository.dart";
// Models
export "src/models/chat_model.dart";
export "src/models/message_model.dart";
export "src/models/user_model.dart";
// Services
export "src/services/chat_service.dart";

View file

@ -1,23 +0,0 @@
/// An exception that is used to indicate the failure to load a chat for given
/// [chatId]
class ChatNotFoundException implements Exception {
/// Create an instance of the chat not found exception
const ChatNotFoundException({
required this.chatId,
});
/// The chat that was attempted to load, but never found.
final String chatId;
}
/// An exception that is used to indicate the failure to load the messages for a
/// given [chatId]
class ChatMessagesNotFoundException implements Exception {
/// Create an instance of the chatmessages not found exception
const ChatMessagesNotFoundException({
required this.chatId,
});
/// The chat for which messages were attempted to load, but never found.
final String chatId;
}

View file

@ -1,25 +0,0 @@
import "dart:convert";
import "dart:typed_data";
import "package:mime/mime.dart";
/// Error thrown when there is no
/// mimetype found
class MimetypeMissingError extends Error {
@override
String toString() => "You can only provide files that contain a mimetype";
}
/// Extension that provides a converter function from
/// Uin8List to a base64Encoded data uri.
extension ToDataUri on Uint8List {
/// This function converts the Uint8List into
/// a uri with a data-scheme.
String toDataUri() {
var mimeType = lookupMimeType("", headerBytes: this);
if (mimeType == null) throw MimetypeMissingError();
var base64Data = base64Encode(this);
return "data:$mimeType;base64,$base64Data";
}
}

View file

@ -1,133 +0,0 @@
import "dart:typed_data";
import "package:chat_repository_interface/src/models/chat_model.dart";
import "package:chat_repository_interface/src/models/message_model.dart";
import "package:chat_repository_interface/src/models/user_model.dart";
/// The chat repository interface
/// Implement this interface to create a chat
/// repository with a given data source.
abstract class ChatRepositoryInterface {
/// Create a chat with the given parameters.
/// [users] is a list of [UserModel] that will be part of the chat.
/// [chatName] is the name of the chat.
/// [description] is the description of the chat.
/// [imageUrl] is the image url of the chat.
/// [messages] is a list of [MessageModel] that will be part of the chat.
Future<void> createChat({
required List<String> users,
required bool isGroupChat,
String? chatName,
String? description,
String? imageUrl,
List<MessageModel>? messages,
});
/// Update the chat with the given parameters.
/// [chat] is the chat that will be updated.
Future<void> updateChat({
required ChatModel chat,
});
/// Get the chat with the given [chatId].
/// Returns a [ChatModel] stream.
Stream<ChatModel> getChat({
required String chatId,
});
/// Get the chats for the given [userId].
/// Returns a list of [ChatModel] stream.
Stream<List<ChatModel>?> getChats({
required String userId,
});
/// Get the messages for the given [chatId].
/// Returns a list of [MessageModel] stream.
/// [userId] is the user id.
/// [chatId] is the chat id.
/// Returns a list of [MessageModel] stream.
Stream<List<MessageModel>?> getMessages({
required String chatId,
required String userId,
});
/// Get the message with the given [messageId].
/// [chatId] is the chat id.
/// Returns a [MessageModel] stream.
Stream<MessageModel?> getMessage({
required String chatId,
required String messageId,
});
/// Signals that new messages should be loaded after the given message.
/// The stream should emit the new messages.
Future<void> loadNewMessagesAfter({
required String userId,
required MessageModel lastMessage,
});
/// Signals that old messages should be loaded before the given message.
/// The stream should emit the new messages.
Future<void> loadOldMessagesBefore({
required String userId,
required MessageModel firstMessage,
});
/// Retrieve the next unused message id given a current chat.
///
/// The resulting string should be at least unique per [chatId]. The userId
/// is provided in case the specific user has influence on the id.
///
/// Imagine returning a UUID, the next integer in a counter or the document
/// id in firebase.
Future<String> getNextMessageId({
required String userId,
required String chatId,
});
/// Send a message with the given parameters.
///
/// [chatId] is the chat id.
/// [senderId] is the sender id.
/// [messageId] is the identifier for this message
/// [text] is the message text.
/// [imageUrl] is the image url.
/// [messageType] is a way to identify a difference in messages
/// [timestamp] is the moment of sending.
Future<void> sendMessage({
required String chatId,
required String senderId,
required String messageId,
String? text,
String? imageUrl,
String? messageType,
DateTime? timestamp,
});
/// Delete the chat with the given [chatId].
Future<void> deleteChat({
required String chatId,
});
/// Get the unread messages count for the given [userId].
/// [chatId] is the chat id. If not provided, it will return the
/// total unread messages count.
/// Returns an integer stream.
Stream<int> getUnreadMessagesCount({
required String userId,
String? chatId,
});
/// Upload an image with the given parameters.
/// [path] is the path of the image.
/// [image] is the image data.
/// [senderId] is the sender id.
/// [chatId] is the chat id.
/// Returns the image url.
Future<String> uploadImage({
required String path,
required String senderId,
required String chatId,
required Uint8List image,
});
}

View file

@ -1,42 +0,0 @@
import "package:chat_repository_interface/src/models/message_model.dart";
/// The pending chat messages repository interface
/// Implement this interface to create a pending chat
/// messages repository with a given data source.
abstract class PendingMessageRepositoryInterface {
/// Get the messages for the given [chatId].
/// Returns a list of [MessageModel] stream.
/// [userId] is the user id.
/// [chatId] is the chat id.
/// Returns a list of [MessageModel] stream.
Stream<List<MessageModel>> getMessages({
required String chatId,
required String userId,
});
/// Create a message in the pending messages and return the created message.
///
/// [chatId] is the chat id.
/// [senderId] is the sender id.
/// [messageId] is the identifier for this message
/// [text] is the message text.
/// [imageUrl] is the image url.
/// [messageType] is a way to identify a difference in messages
/// [timestamp] is the moment of sending.
Future<MessageModel> createMessage({
required String chatId,
required String senderId,
required String messageId,
String? text,
String? imageUrl,
String? messageType,
DateTime? timestamp,
});
/// Mark a message as being succesfully sent to the server,
/// so that it can be removed from this data source.
Future<void> markMessageSent({
required String chatId,
required String messageId,
});
}

View file

@ -1,18 +0,0 @@
import "package:chat_repository_interface/src/models/user_model.dart";
/// The user repository interface
/// Implement this interface to create a user
/// repository with a given data source.
abstract class UserRepositoryInterface {
/// Get the user with the given [userId].
/// Returns a [UserModel] stream.
Stream<UserModel> getUser({required String userId});
/// Get all the users.
/// Returns a list of [UserModel] stream.
Stream<List<UserModel>> getAllUsers();
/// Get all the users for the given [chatId].
/// Returns a list of [UserModel] stream.
Stream<List<UserModel>> getAllUsersForChat({required String chatId});
}

View file

@ -1,290 +0,0 @@
import "dart:async";
import "dart:math" as math;
import "dart:typed_data";
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:chat_repository_interface/src/extension/uint8list_data_uri.dart";
import "package:chat_repository_interface/src/local/local_memory_db.dart";
import "package:collection/collection.dart";
import "package:rxdart/rxdart.dart";
/// The local chat repository
class LocalChatRepository implements ChatRepositoryInterface {
/// The local chat repository constructor
LocalChatRepository();
final StreamController<List<ChatModel>> _chatsController =
BehaviorSubject<List<ChatModel>>();
final StreamController<ChatModel> _chatController =
BehaviorSubject<ChatModel>();
final StreamController<List<MessageModel>> _messageController =
BehaviorSubject<List<MessageModel>>();
final Map<String, int> _startIndexMap = {};
final Map<String, int> _endIndexMap = {};
@override
Future<void> createChat({
required List<String> users,
required bool isGroupChat,
String? chatName,
String? description,
String? imageUrl,
List<MessageModel>? messages,
}) async {
var chat = ChatModel(
id: DateTime.now().toString(),
isGroupChat: isGroupChat,
users: users,
chatName: chatName,
description: description,
imageUrl: imageUrl,
);
chats.add(chat);
_chatsController.add(chats);
if (messages != null) {
for (var message in messages) {
await sendMessage(
messageId: message.id,
chatId: chat.id,
senderId: message.senderId,
text: message.text,
messageType: message.messageType,
imageUrl: message.imageUrl,
timestamp: message.timestamp,
);
}
}
}
@override
Future<void> updateChat({
required ChatModel chat,
}) async {
var index = chats.indexWhere((e) => e.id == chat.id);
if (index != -1) {
chats[index] = chat;
_chatsController.add(chats);
}
}
@override
Future<void> deleteChat({
required String chatId,
}) async {
try {
chats.removeWhere((e) => e.id == chatId);
_chatsController.add(chats);
} on Exception catch (_) {
rethrow;
}
}
@override
Stream<ChatModel> getChat({
required String chatId,
}) {
var chat = chats.firstWhereOrNull((e) => e.id == chatId);
if (chat != null) {
_chatController.add(chat);
if (chat.imageUrl?.isNotEmpty ?? false) {
chat.copyWith(imageUrl: "https://picsum.photos/200/300");
}
}
return _chatController.stream;
}
@override
Stream<List<ChatModel>?> getChats({
required String userId,
}) {
_chatsController.add(chats);
return _chatsController.stream;
}
@override
Stream<List<MessageModel>?> getMessages({
required String chatId,
required String userId,
}) {
var foundChat =
chats.firstWhereOrNull((chatModel) => chatModel.id == chatId);
if (foundChat == null) {
_messageController.add([]);
} else {
var allMessages = List<MessageModel>.from(
chatMessages[chatId] ?? [],
);
allMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
_startIndexMap[chatId] ??= math.max(0, allMessages.length - chunkSize);
_endIndexMap[chatId] ??= allMessages.length;
var displayedMessages = allMessages.sublist(
_startIndexMap[chatId]!,
_endIndexMap[chatId],
);
_messageController.add(displayedMessages);
}
return _messageController.stream;
}
@override
Future<void> loadNewMessagesAfter({
required String userId,
required MessageModel lastMessage,
}) async {
var allMessages = List<MessageModel>.from(
chatMessages[lastMessage.chatId] ?? [],
)..sort((a, b) => a.timestamp.compareTo(b.timestamp));
var lastMessageIndex = allMessages
.indexWhere((messageModel) => messageModel.id == lastMessage.id);
if (lastMessageIndex == -1) {
return;
}
var currentEndIndex =
_endIndexMap[lastMessage.chatId] ?? allMessages.length;
_endIndexMap[lastMessage.chatId] = math.min(
allMessages.length,
currentEndIndex + chunkSize,
);
var displayedMessages = allMessages.sublist(
_startIndexMap[lastMessage.chatId] ?? 0,
_endIndexMap[lastMessage.chatId],
);
_messageController.add(displayedMessages);
}
@override
Future<void> loadOldMessagesBefore({
required String userId,
required MessageModel firstMessage,
}) async {
var allMessages = List<MessageModel>.from(
chatMessages[firstMessage.chatId] ?? [],
)..sort((a, b) => a.timestamp.compareTo(b.timestamp));
var firstMessageIndex = allMessages
.indexWhere((messageModel) => messageModel.id == firstMessage.id);
if (firstMessageIndex == -1) {
return;
}
var currentStartIndex = _startIndexMap[firstMessage.chatId] ?? 0;
_startIndexMap[firstMessage.chatId] = math.max(
0,
currentStartIndex - chunkSize,
);
var displayedMessages = allMessages.sublist(
_startIndexMap[firstMessage.chatId]!,
_endIndexMap[firstMessage.chatId] ?? allMessages.length,
);
_messageController.add(displayedMessages);
}
@override
Stream<MessageModel?> getMessage({
required String chatId,
required String messageId,
}) {
var message =
chatMessages[chatId]?.firstWhereOrNull((e) => e.id == messageId);
return Stream.value(message);
}
@override
Future<String> getNextMessageId({
required String userId,
required String chatId,
}) async =>
"$chatId-$userId-${DateTime.now()}";
@override
Future<void> sendMessage({
required String chatId,
required String senderId,
required String messageId,
String? text,
String? imageUrl,
String? messageType,
DateTime? timestamp,
}) async {
var message = MessageModel(
chatId: chatId,
id: messageId,
timestamp: timestamp ?? DateTime.now(),
text: text,
messageType: messageType,
senderId: senderId,
imageUrl: imageUrl,
status: MessageStatus.sent,
);
var chat = chats.firstWhereOrNull((e) => e.id == chatId);
if (chat == null) throw Exception("Chat not found");
var messages = List<MessageModel>.from(chatMessages[chatId] ?? []);
messages.add(message);
chatMessages[chatId] = messages;
var newChat = chat.copyWith(
lastMessage: messageId,
unreadMessageCount: chat.unreadMessageCount + 1,
lastUsed: DateTime.now(),
);
chats[chats.indexWhere((e) => e.id == chatId)] = newChat;
_chatsController.add(chats);
_messageController.add(chatMessages[chatId] ?? []);
}
@override
Stream<int> getUnreadMessagesCount({
required String userId,
String? chatId,
}) =>
_chatsController.stream.map((chats) {
var count = 0;
for (var chat in chats) {
if (chat.users.contains(userId)) {
count += chat.unreadMessageCount;
}
}
return count;
});
@override
Future<String> uploadImage({
required String path,
required Uint8List image,
required String chatId,
required String senderId,
}) =>
Future.value(image.toDataUri());
/// All the chats of the local memory database
List<ChatModel> get getLocalChats => chats;
/// The chunkSize used for pagination
int get getChunkSize => chunkSize;
}

View file

@ -1,43 +0,0 @@
import "package:chat_repository_interface/src/models/chat_model.dart";
import "package:chat_repository_interface/src/models/message_model.dart";
import "package:chat_repository_interface/src/models/user_model.dart";
/// The chunkSize for the LocalChatRepository
const int chunkSize = 10;
/// All the chats of the local memory database
final List<ChatModel> chats = [];
/// All the messages of the local memory database mapped by chat id
final Map<String, List<MessageModel>> chatMessages = {};
/// All the pending messages of the local memory database mapped by chat id
final Map<String, List<MessageModel>> pendingChatMessages = {};
/// All the users of the local memory database
final List<UserModel> users = [
const UserModel(
id: "1",
firstName: "John",
lastName: "Doe",
imageUrl: "https://picsum.photos/200/300",
),
const UserModel(
id: "2",
firstName: "Jane",
lastName: "Doe",
imageUrl: "https://picsum.photos/200/300",
),
const UserModel(
id: "3",
firstName: "Frans",
lastName: "Timmermans",
imageUrl: "https://picsum.photos/200/300",
),
const UserModel(
id: "4",
firstName: "Hendrik-Jan",
lastName: "De derde",
imageUrl: "https://picsum.photos/200/300",
),
];

View file

@ -1,90 +0,0 @@
import "dart:async";
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:chat_repository_interface/src/local/local_memory_db.dart";
import "package:collection/collection.dart";
import "package:rxdart/rxdart.dart";
/// The local pending message repository
class LocalPendingMessageRepository
implements PendingMessageRepositoryInterface {
/// The local pending message repository constructor
LocalPendingMessageRepository();
final StreamController<List<MessageModel>> _messageController =
BehaviorSubject<List<MessageModel>>();
@override
Stream<List<MessageModel>> getMessages({
required String chatId,
required String userId,
}) {
var foundChat =
chats.firstWhereOrNull((chatModel) => chatModel.id == chatId);
if (foundChat == null) {
_messageController.add([]);
} else {
var allMessages = List<MessageModel>.from(
pendingChatMessages[chatId] ?? [],
);
allMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
_messageController.add(allMessages);
}
return _messageController.stream;
}
Future<void> _chatExists(String chatId) async {
var chat = chats.firstWhereOrNull((e) => e.id == chatId);
if (chat == null) throw Exception("Chat not found");
}
@override
Future<MessageModel> createMessage({
required String chatId,
required String senderId,
required String messageId,
String? text,
String? imageUrl,
String? messageType,
DateTime? timestamp,
}) async {
var message = MessageModel(
chatId: chatId,
id: messageId,
timestamp: timestamp ?? DateTime.now(),
text: text,
messageType: messageType,
senderId: senderId,
imageUrl: imageUrl,
status: MessageStatus.sending,
);
await _chatExists(chatId);
var messages = List<MessageModel>.from(pendingChatMessages[chatId] ?? []);
messages.add(message);
pendingChatMessages[chatId] = messages;
_messageController.add(pendingChatMessages[chatId] ?? []);
return message;
}
@override
Future<void> markMessageSent({
required String chatId,
required String messageId,
}) async {
await _chatExists(chatId);
var messages = List<MessageModel>.from(pendingChatMessages[chatId] ?? []);
MessageModel markSent(MessageModel message) =>
(message.id == messageId) ? message.markSent() : message;
pendingChatMessages[chatId] = messages.map(markSent).toList();
}
}

View file

@ -1,53 +0,0 @@
import "dart:async";
import "package:chat_repository_interface/src/interfaces/user_repository_interface.dart";
import "package:chat_repository_interface/src/local/local_memory_db.dart";
import "package:chat_repository_interface/src/models/user_model.dart";
import "package:rxdart/rxdart.dart";
/// The local user repository
class LocalUserRepository implements UserRepositoryInterface {
final StreamController<List<UserModel>> _usersController =
BehaviorSubject<List<UserModel>>();
@override
Stream<UserModel> getUser({
required String userId,
}) =>
getAllUsers().map(
(users) => users.firstWhere(
(e) => e.id == userId,
orElse: () => throw Exception(),
),
);
@override
Stream<List<UserModel>> getAllUsers() {
_usersController.add(users);
return _usersController.stream;
}
@override
Stream<List<UserModel>> getAllUsersForChat({
required String chatId,
}) =>
Stream.value(
chats
.firstWhere(
(chat) => chat.id == chatId,
orElse: () => throw Exception("Chat not found"),
)
.users
.map(
(userId) => users.firstWhere(
(user) => user.id == userId,
orElse: () => throw Exception("User not found"),
),
)
.toList(),
);
/// All the users of the local memory database
List<UserModel> get getLocalUsers => users;
}

View file

@ -1,121 +0,0 @@
/// The chat model
/// A model that represents a chat.
/// [id] is the chat id.
/// [users] is a list of [UserModel] that are part of the chat.
/// [chatName] is the name of the chat.
/// [description] is the description of the chat.
/// [imageUrl] is the image url of the chat.
/// [canBeDeleted] is a boolean that indicates if the chat can be deleted.
/// [lastUsed] is the last time the chat was used.
/// [lastMessage] is the last message of the chat.
/// [unreadMessageCount] is the number of unread messages in the chat.
/// Returns a [ChatModel] instance.
class ChatModel {
/// The chat model constructor
const ChatModel({
required this.id,
required this.users,
required this.isGroupChat,
this.chatName,
this.description,
this.imageUrl,
this.canBeDeleted = true,
this.lastUsed,
this.lastMessage,
this.unreadMessageCount = 0,
});
/// The factory chat model that creates a chat model from a map
factory ChatModel.fromMap(String id, Map<String, dynamic> data) => ChatModel(
id: id,
users: List<String>.from(data["users"]),
isGroupChat: data["isGroupChat"],
chatName: data["chatName"],
description: data["description"],
imageUrl: data["imageUrl"],
canBeDeleted: data["canBeDeleted"] ?? true,
lastUsed: data["lastUsed"] != null
? DateTime.fromMillisecondsSinceEpoch(data["lastUsed"])
: null,
lastMessage: data["lastMessage"],
unreadMessageCount: data["unreadMessageCount"] ?? 0,
);
/// The chat id
final String id;
/// The chat users
final List<String> users;
/// The chat name
final String? chatName;
/// The chat description
final String? description;
/// The chat image url
final String? imageUrl;
/// A boolean that indicates if the chat can be deleted
final bool canBeDeleted;
/// The last time the chat was used
final DateTime? lastUsed;
/// The last message of the chat
final String? lastMessage;
/// The number of unread messages in the chat
final int unreadMessageCount;
/// A boolean that indicates if the chat is a group chat
final bool isGroupChat;
/// The chat model copy with method
ChatModel copyWith({
String? id,
List<String>? users,
String? chatName,
String? description,
String? imageUrl,
bool? canBeDeleted,
DateTime? lastUsed,
String? lastMessage,
int? unreadMessageCount,
bool? isGroupChat,
}) =>
ChatModel(
id: id ?? this.id,
users: users ?? this.users,
chatName: chatName ?? this.chatName,
isGroupChat: isGroupChat ?? this.isGroupChat,
description: description ?? this.description,
imageUrl: imageUrl ?? this.imageUrl,
canBeDeleted: canBeDeleted ?? this.canBeDeleted,
lastUsed: lastUsed ?? this.lastUsed,
lastMessage: lastMessage ?? this.lastMessage,
unreadMessageCount: unreadMessageCount ?? this.unreadMessageCount,
);
/// Creates a map representation of this object
Map<String, dynamic> toMap() => {
"users": users,
"isGroupChat": isGroupChat,
"chatName": chatName,
"description": description,
"imageUrl": imageUrl,
"canBeDeleted": canBeDeleted,
"lastUsed": lastUsed?.millisecondsSinceEpoch,
"lastMessage": lastMessage,
"unreadMessageCount": unreadMessageCount,
};
}
/// The chat model extension
/// An extension that adds extra functionality to the chat model.
/// [getOtherUser] is a method that returns the other user in the chat.
extension GetOtherUser on ChatModel {
/// The get other user method
String getOtherUser(String userId) =>
users.firstWhere((user) => user != userId);
}

View file

@ -1,123 +0,0 @@
/// Message status enumeration
enum MessageStatus {
/// Status when a message has not yet been received by the server.
sending,
/// Status used when a message has been received by the server.
sent;
/// Attempt to parse [MessageStatus] from String
static MessageStatus? tryParse(String name) =>
MessageStatus.values.where((status) => status.name == name).firstOrNull;
/// Parse [MessageStatus] from String
/// or throw a [FormatException]
static MessageStatus parse(String name) =>
tryParse(name) ??
(throw const FormatException(
"MessageStatus with that name does not exist",
));
}
/// Message model
/// Represents a message in a chat
/// [id] is the message id.
/// [text] is the message text.
/// [imageUrl] is the message image url.
/// [timestamp] is the message timestamp.
/// [senderId] is the sender id.
class MessageModel {
/// Message model constructor
const MessageModel({
required this.chatId,
required this.id,
required this.text,
required this.messageType,
required this.imageUrl,
required this.timestamp,
required this.senderId,
this.status = MessageStatus.sent,
});
/// Creates a message model instance given a map instance
factory MessageModel.fromMap(String id, Map<String, dynamic> map) =>
MessageModel(
chatId: map["chatId"],
id: id,
text: map["text"],
messageType: map["messageType"],
imageUrl: map["imageUrl"],
timestamp: DateTime.fromMillisecondsSinceEpoch(map["timestamp"]),
senderId: map["senderId"],
status: MessageStatus.tryParse(map["status"]) ?? MessageStatus.sent,
);
/// The chat id
final String chatId;
/// The message id
final String id;
/// The message text
final String? text;
/// The type of message for instance (user, system, etc)
final String? messageType;
/// The message image url
final String? imageUrl;
/// The message timestamp
final DateTime timestamp;
/// The sender id
final String senderId;
/// The message status
final MessageStatus status;
/// The message model copy with method
MessageModel copyWith({
String? chatId,
String? id,
String? text,
String? messageType,
String? imageUrl,
DateTime? timestamp,
String? senderId,
MessageStatus? status,
}) =>
MessageModel(
chatId: chatId ?? this.chatId,
id: id ?? this.id,
text: text ?? this.text,
messageType: messageType ?? this.messageType,
imageUrl: imageUrl ?? this.imageUrl,
timestamp: timestamp ?? this.timestamp,
senderId: senderId ?? this.senderId,
status: status ?? this.status,
);
/// Creates a map representation of this object
Map<String, dynamic> toMap() => {
"chatId": chatId,
"text": text,
"messageType": messageType,
"imageUrl": imageUrl,
"timestamp": timestamp.millisecondsSinceEpoch,
"senderId": senderId,
"status": status.name,
};
/// marks the message model as sent
MessageModel markSent() => copyWith(status: MessageStatus.sent);
}
/// Extension on [MessageModel] to check the message type
extension MessageType on MessageModel {
/// Check if the message is a text message
bool get isTextMessage => text != null;
/// Check if the message is an image message
bool get isImageMessage => imageUrl != null;
}

View file

@ -1,56 +0,0 @@
/// User model
/// Represents a user in a chat
/// [id] is the user id.
/// [firstName] is the user first name.
/// [lastName] is the user last name.
/// [imageUrl] is the user image url.
/// [fullname] is the user full name.
class UserModel {
/// User model constructor
const UserModel({
required this.id,
this.firstName,
this.lastName,
this.imageUrl,
});
/// Creates a user based on a given map [data]
factory UserModel.fromMap(String id, Map<String, dynamic> data) => UserModel(
id: id,
firstName: data["first_name"],
lastName: data["last_name"],
imageUrl: data["image_url"],
);
/// The user id
final String id;
/// The user first name
final String? firstName;
/// The user last name
final String? lastName;
/// The user image url
final String? imageUrl;
}
/// Extension on [UserModel] to get the user full name
extension Fullname on UserModel {
/// Get the user full name
String? get fullname {
if (firstName == null && lastName == null) {
return null;
}
if (firstName == null) {
return lastName;
}
if (lastName == null) {
return firstName;
}
return "$firstName $lastName";
}
}

View file

@ -1,343 +0,0 @@
import "dart:async";
import "dart:typed_data";
import "package:chat_repository_interface/src/extension/uint8list_data_uri.dart";
import "package:chat_repository_interface/src/interfaces/chat_repostory_interface.dart";
import "package:chat_repository_interface/src/interfaces/pending_message_repository_interface.dart";
import "package:chat_repository_interface/src/interfaces/user_repository_interface.dart";
import "package:chat_repository_interface/src/local/local_chat_repository.dart";
import "package:chat_repository_interface/src/local/local_pending_message_repository.dart";
import "package:chat_repository_interface/src/local/local_user_repository.dart";
import "package:chat_repository_interface/src/models/chat_model.dart";
import "package:chat_repository_interface/src/models/message_model.dart";
import "package:chat_repository_interface/src/models/user_model.dart";
import "package:collection/collection.dart";
import "package:rxdart/rxdart.dart";
/// The chat service
/// Use this service to interact with the chat repository.
/// Optionally provide a [chatRepository] and [userRepository]
class ChatService {
/// Create a chat service with the given parameters.
ChatService({
required this.userId,
ChatRepositoryInterface? chatRepository,
PendingMessageRepositoryInterface? pendingMessageRepository,
UserRepositoryInterface? userRepository,
}) : chatRepository = chatRepository ?? LocalChatRepository(),
userRepository = userRepository ?? LocalUserRepository(),
pendingMessageRepository =
pendingMessageRepository ?? LocalPendingMessageRepository();
/// The user ID of the person currently looking at the chat
final String userId;
/// The chat repository
final ChatRepositoryInterface chatRepository;
/// The pending messages repository
final PendingMessageRepositoryInterface pendingMessageRepository;
/// The user repository
final UserRepositoryInterface userRepository;
/// Create a chat with the given parameters.
/// [users] is a list of [UserModel] that will be part of the chat.
/// [chatName] is the name of the chat.
/// [description] is the description of the chat.
/// [imageUrl] is the image url of the chat.
/// [messages] is a list of [MessageModel] that will be part of the chat.
/// Returns a [ChatModel] stream.
Future<void> createChat({
required List<UserModel> users,
required bool isGroupChat,
String? chatName,
String? description,
String? imageUrl,
List<MessageModel>? messages,
}) {
var userIds = users.map((e) => e.id).toList();
return chatRepository.createChat(
isGroupChat: isGroupChat,
users: userIds,
chatName: chatName,
description: description,
imageUrl: imageUrl,
messages: messages,
);
}
/// Get the chats for the user with the given [userId].
/// Returns a list of [ChatModel] stream.
Stream<List<ChatModel>?> getChats() =>
chatRepository.getChats(userId: userId);
/// Get the chat with the given [chatId].
/// Returns a [ChatModel] stream.
Stream<ChatModel> getChat({
required String chatId,
}) =>
chatRepository.getChat(chatId: chatId);
/// Get the chat with the given [currentUser] and [otherUser].
/// Returns a [ChatModel] stream.
/// Returns null if the chat does not exist.
Future<ChatModel?> getChatByUser({
required String currentUser,
required String otherUser,
}) async {
var chats = await chatRepository.getChats(userId: currentUser).first;
var personalChats =
chats?.where((element) => element.users.length == 2).toList();
return personalChats?.firstWhereOrNull(
(element) => element.users.where((e) => e == otherUser).isNotEmpty,
);
}
/// Get the group chats with the given [currentUser] and [otherUsers].
/// Returns a [ChatModel] stream.
Future<ChatModel?> getGroupChatByUser({
required String currentUser,
required List<UserModel> otherUsers,
required String chatName,
required String description,
}) async {
try {
var chats = await chatRepository.getChats(userId: currentUser).first;
var personalChats =
chats?.where((element) => element.isGroupChat).toList();
var groupChats = personalChats
?.where(
(chats) =>
otherUsers.every((user) => chats.users.contains(user.id)),
)
.toList();
return groupChats?.firstWhereOrNull(
(element) =>
element.chatName == chatName && element.description == description,
);
// ignore: avoid_catches_without_on_clauses
} catch (_) {
throw Exception("Chat not found");
}
}
/// Get the message with the given [messageId].
/// [chatId] is the chat id.
/// Returns a [MessageModel] stream.
Stream<MessageModel?> getMessage({
required String chatId,
required String messageId,
}) =>
chatRepository.getMessage(chatId: chatId, messageId: messageId);
/// Get the messages for the given [chatId].
/// Returns a list of [MessageModel] stream.
/// [pageSize] is the number of messages to be fetched.
/// [page] is the page number.
/// [chatId] is the chat id.
/// Returns a list of [MessageModel] stream.
Stream<List<MessageModel>?> getMessages({
required String chatId,
}) {
List<MessageModel> mergePendingMessages(
List<MessageModel> messages,
List<MessageModel> pendingMessages,
) =>
{
...Map.fromEntries(
pendingMessages.map((message) => MapEntry(message.id, message)),
),
...Map.fromEntries(
messages.map((message) => MapEntry(message.id, message)),
),
}.values.toList().sorted(
(a, b) => a.timestamp.compareTo(b.timestamp),
);
return Rx.combineLatest2(
chatRepository.getMessages(userId: userId, chatId: chatId),
pendingMessageRepository.getMessages(userId: userId, chatId: chatId),
(chatMessages, pendingChatMessages) {
// TODO(Quirille): This is because chatRepository.getMessages
// might return null, when really it should've just thrown
// an exception instead.
if (chatMessages == null) return null;
return mergePendingMessages(chatMessages, pendingChatMessages);
},
);
}
/// Signals that new messages should be loaded after the given message.
/// The stream should emit the new messages.
Future<void> loadNewMessagesAfter({
required MessageModel lastMessage,
}) =>
chatRepository.loadNewMessagesAfter(
userId: userId,
lastMessage: lastMessage,
);
/// Signals that old messages should be loaded before the given message.
/// The stream should emit the new messages.
Future<void> loadOldMessagesBefore({
required MessageModel firstMessage,
}) =>
chatRepository.loadOldMessagesBefore(
userId: userId,
firstMessage: firstMessage,
);
/// Send a message with the given parameters.
/// [chatId] is the chat id.
/// [senderId] is the sender id.
/// [text] is the message text.
/// [imageUrl] is the image url.
Future<void> sendMessage({
required String chatId,
required String senderId,
String? presetMessageId,
String? text,
String? messageType,
String? imageUrl,
Uint8List? imageData,
}) async {
var messageId = presetMessageId ??
await chatRepository.getNextMessageId(userId: userId, chatId: chatId);
await pendingMessageRepository.createMessage(
chatId: chatId,
senderId: senderId,
messageId: messageId,
text: text,
messageType: messageType,
imageUrl: imageData?.toDataUri() ?? imageUrl,
);
unawaited(
chatRepository
.sendMessage(
chatId: chatId,
messageId: messageId,
text: text,
messageType: messageType,
senderId: senderId,
imageUrl: imageUrl,
)
.then(
(_) => pendingMessageRepository.markMessageSent(
chatId: chatId,
messageId: messageId,
),
)
.onError(
(e, s) {
// TODO(Quirille): handle exception when message sending has failed.
},
),
);
}
/// Method for sending an image and a message at the same time.
Future<void> sendImageMessage({
required String chatId,
required String userId,
required Uint8List data,
}) async {
var messageId = await chatRepository.getNextMessageId(
userId: userId,
chatId: chatId,
);
var path = await uploadImage(
path: "chats/$messageId",
image: data,
chatId: chatId,
);
await sendMessage(
presetMessageId: messageId,
chatId: chatId,
senderId: userId,
imageUrl: path,
imageData: data,
);
}
/// Delete the chat with the given parameters.
/// [chatId] is the chat id.
Future<void> deleteChat({
required String chatId,
}) =>
chatRepository.deleteChat(chatId: chatId);
/// Get user with the given [userId].
/// Returns a [UserModel] stream.
Stream<UserModel> getUser({required String userId}) =>
userRepository.getUser(userId: userId);
/// Get all the users.
/// Returns a list of [UserModel] stream.
Stream<List<UserModel>> getAllUsers() => userRepository.getAllUsers();
/// Get the unread messages count for a user [chatId].
/// [chatId] is the chat id. If not provided, it will return the
/// total unread messages count.
/// Returns a [Stream] of [int].
Stream<int> getUnreadMessagesCount({
String? chatId,
}) =>
chatRepository.getUnreadMessagesCount(userId: userId, chatId: chatId);
/// Upload an image with the given parameters.
/// [path] is the image path.
/// [image] is the image bytes.
/// [chatId] is the chat id.
/// Returns a [Future] of [String].
Future<String> uploadImage({
required String path,
required Uint8List image,
required String chatId,
}) =>
chatRepository.uploadImage(
path: path,
image: image,
senderId: userId,
chatId: chatId,
);
/// Mark the chat as read with the given parameters.
/// [chatId] is the chat id.
/// Returns a [Future] of [void].
Future<void> markAsRead({
required String chatId,
}) async {
var chat = await chatRepository.getChat(chatId: chatId).first;
if (chat.lastMessage == null) return;
var lastMessage = await chatRepository
.getMessage(chatId: chatId, messageId: chat.lastMessage!)
.first;
if (lastMessage != null && lastMessage.senderId == userId) return;
var newChat = chat.copyWith(
lastUsed: DateTime.now(),
unreadMessageCount: 0,
);
await chatRepository.updateChat(chat: newChat);
}
/// Get all the users for the given [chatId].
/// Returns a list of [UserModel] stream.
Stream<List<UserModel>> getAllUsersForChat({required String chatId}) =>
userRepository.getAllUsersForChat(chatId: chatId);
}

View file

@ -1,21 +0,0 @@
name: chat_repository_interface
description: "The interface for a chat repository"
version: 6.0.0
homepage: "https://github.com/Iconica-Development"
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
environment:
sdk: ">=3.4.3 <4.0.0"
dependencies:
mime: any
rxdart: any
collection: any
dev_dependencies:
test: ^1.24.0
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0

View file

@ -1,29 +0,0 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
build/

View file

@ -1 +0,0 @@
../../CHANGELOG.md

View file

@ -1 +0,0 @@
../../LICENSE

View file

@ -1,16 +0,0 @@
# Firebase chat repository
The firebase implementation of the chat_repository_interface
## Usage
```dart
chatService: ChatService(
chatRepository: FirebaseChatRepository(
chatCollection: 'chats',
messageCollection: 'messages',
mediaPath: 'chat',
),
userRepository: FirebaseUserRepository(
userCollection: 'users',
),
),
```

View file

@ -1,9 +0,0 @@
include: package:flutter_iconica_analysis/components_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -1,2 +0,0 @@
export "src/firebase_chat_repository.dart";
export "src/firebase_user_repository.dart";

View file

@ -1,220 +0,0 @@
import "dart:typed_data";
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:cloud_firestore/cloud_firestore.dart";
import "package:firebase_storage/firebase_storage.dart";
/// Firebase implementation of the chat repository
class FirebaseChatRepository implements ChatRepositoryInterface {
/// Creates a firebase implementation of the chat repository
FirebaseChatRepository({
FirebaseFirestore? firestore,
FirebaseStorage? storage,
String chatCollection = "chats",
String messageCollection = "messages",
String mediaPath = "chat",
}) : _mediaPath = mediaPath,
_messageCollection = messageCollection,
_chatCollection = chatCollection,
_firestore = firestore ?? FirebaseFirestore.instance,
_storage = storage ?? FirebaseStorage.instance;
final FirebaseFirestore _firestore;
final FirebaseStorage _storage;
final String _chatCollection;
final String _messageCollection;
final String _mediaPath;
@override
Future<void> createChat({
required List<String> users,
required bool isGroupChat,
String? chatName,
String? description,
String? imageUrl,
List<MessageModel>? messages,
}) async {
var chatData = {
"users": users,
"isGroupChat": isGroupChat,
"chatName": chatName,
"description": description,
"imageUrl": imageUrl,
"createdAt": DateTime.now().millisecondsSinceEpoch,
};
await _firestore.collection(_chatCollection).add(chatData);
}
@override
Future<void> deleteChat({required String chatId}) async {
await _firestore.collection(_chatCollection).doc(chatId).delete();
}
@override
Stream<ChatModel> getChat({required String chatId}) => _firestore
.collection(_chatCollection)
.doc(chatId)
.snapshots()
.map((snapshot) {
var data = snapshot.data()!;
return ChatModel.fromMap(snapshot.id, data);
});
@override
Stream<List<ChatModel>?> getChats({required String userId}) => _firestore
.collection(_chatCollection)
.where("users", arrayContains: userId)
.snapshots()
.map(
(querySnapshot) => querySnapshot.docs.map((doc) {
var data = doc.data();
return ChatModel.fromMap(doc.id, data);
}).toList(),
);
@override
Stream<MessageModel?> getMessage({
required String chatId,
required String messageId,
}) =>
_firestore
.collection(_chatCollection)
.doc(chatId)
.collection(_messageCollection)
.doc(messageId)
.snapshots()
.map((snapshot) {
var data = snapshot.data()!;
return MessageModel.fromMap(
snapshot.id,
data,
);
});
@override
Stream<List<MessageModel>?> getMessages({
required String chatId,
required String userId,
}) =>
_firestore
.collection(_chatCollection)
.doc(chatId)
.collection(_messageCollection)
.orderBy("timestamp")
.snapshots()
.map(
(query) => query.docs
.map(
(snapshot) => MessageModel.fromMap(
snapshot.id,
snapshot.data(),
),
)
.toList(),
);
@override
Stream<int> getUnreadMessagesCount({
required String userId,
String? chatId,
}) async* {
var query = _firestore
.collection(_chatCollection)
.where("users", arrayContains: userId)
.where("unreadMessageCount", isGreaterThan: 0)
.snapshots();
await for (var snapshot in query) {
var count = 0;
for (var doc in snapshot.docs) {
var data = doc.data();
var lastMessageKey = data["lastMessage"];
var message =
await getMessage(chatId: doc.id, messageId: lastMessageKey).first;
if (message?.senderId != userId) {
count += data["unreadMessageCount"] as int;
}
}
yield count;
}
}
@override
Future<String> getNextMessageId({
required String userId,
required String chatId,
}) async =>
"$chatId-$userId-${DateTime.now()}";
@override
Future<void> sendMessage({
required String chatId,
required String senderId,
required String messageId,
String? text,
String? imageUrl,
String? messageType,
DateTime? timestamp,
}) async {
var message = MessageModel(
chatId: chatId,
id: messageId,
text: text,
imageUrl: imageUrl,
messageType: messageType,
timestamp: timestamp ?? DateTime.now(),
senderId: senderId,
);
await _firestore
.collection(_chatCollection)
.doc(chatId)
.collection(_messageCollection)
.doc(messageId)
.set(
message.toMap(),
);
await _firestore.collection(_chatCollection).doc(chatId).update(
{
"lastMessage": messageId,
"unreadMessageCount": FieldValue.increment(1),
"lastUsed": DateTime.now().millisecondsSinceEpoch,
},
);
}
@override
Future<void> updateChat({required ChatModel chat}) async {
await _firestore
.collection(_chatCollection)
.doc(chat.id)
.update(chat.toMap());
}
@override
Future<String> uploadImage({
required String path,
required Uint8List image,
required String chatId,
required String senderId,
}) async {
var ref = _storage.ref().child(_mediaPath).child(path);
var uploadTask = ref.putData(image);
var snapshot = await uploadTask.whenComplete(() => {});
return snapshot.ref.getDownloadURL();
}
@override
Future<void> loadNewMessagesAfter({
required String userId,
required MessageModel lastMessage,
}) async {}
@override
Future<void> loadOldMessagesBefore({
required String userId,
required MessageModel firstMessage,
}) async {}
}

View file

@ -1,53 +0,0 @@
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:cloud_firestore/cloud_firestore.dart";
/// Firebase implementation of a user respository for chats.
class FirebaseUserRepository implements UserRepositoryInterface {
/// Creates a firebase implementation of a user respository for chats.
FirebaseUserRepository({
FirebaseFirestore? firestore,
String userCollection = "users",
}) : _userCollection = userCollection,
_firestore = firestore ?? FirebaseFirestore.instance;
final FirebaseFirestore _firestore;
final String _userCollection;
@override
Stream<List<UserModel>> getAllUsers() =>
_firestore.collection(_userCollection).snapshots().map(
(querySnapshot) => querySnapshot.docs
.map(
(doc) => UserModel.fromMap(
doc.id,
doc.data(),
),
)
.toList(),
);
@override
Stream<UserModel> getUser({required String userId}) =>
_firestore.collection(_userCollection).doc(userId).snapshots().map(
(snapshot) => UserModel.fromMap(
snapshot.id,
snapshot.data()!,
),
);
@override
Stream<List<UserModel>> getAllUsersForChat({required String chatId}) =>
_firestore
.collection(_userCollection)
.where("chats", arrayContains: chatId)
.snapshots()
.map(
(querySnapshot) => querySnapshot.docs
.map(
(doc) => UserModel.fromMap(
doc.id,
doc.data(),
),
)
.toList(),
);
}

View file

@ -1,29 +0,0 @@
name: firebase_chat_repository
description: "Firebase repository implementation for the chat domain repository interface"
version: 6.0.0
homepage: "https://github.com/Iconica-Development"
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
environment:
sdk: ">=3.4.3 <4.0.0"
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
chat_repository_interface:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^6.0.0
firebase_storage: any
cloud_firestore: any
dev_dependencies:
flutter_test:
sdk: flutter
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0

View file

@ -1,29 +0,0 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
build/

View file

@ -1 +0,0 @@
../../CHANGELOG.md

View file

@ -1 +0,0 @@
../../LICENSE

View file

@ -1,9 +0,0 @@
include: package:flutter_iconica_analysis/components_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -1,16 +0,0 @@
# example
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View file

@ -1,28 +0,0 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -1 +0,0 @@
{"flutter":{"platforms":{"dart":{"lib/firebase_options.dart":{"projectId":"appshell-demo","configurations":{"android":"1:431820621472:android:6a0f2bc3559d17781babc5","web":"1:431820621472:web:f4b27eea24be24fd1babc5"}}},"android":{"default":{"projectId":"appshell-demo","appId":"1:431820621472:android:6a0f2bc3559d17781babc5","fileOutput":"android/app/google-services.json"}}}}}

View file

@ -1,63 +0,0 @@
// import 'package:example/firebase_options.dart';
// import 'package:firebase_auth/firebase_auth.dart';
// import 'package:firebase_chat_repository/firebase_chat_repository.dart';
// import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chat/flutter_chat.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
useMaterial3: true,
),
home: const MyHomePage(),
// home: FutureBuilder(
// future: Firebase.initializeApp(
// options: DefaultFirebaseOptions.currentPlatform,
// ),
// builder: (context, snapshot) {
// if (snapshot.connectionState != ConnectionState.done) {
// return const Center(
// child: CircularProgressIndicator(),
// );
// }
// return const MyHomePage();
// },
// ),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// @override
// void initState() {
// FirebaseAuth.instance.signInAnonymously();
// super.initState();
// }
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const Center(),
floatingActionButton: const FlutterChatEntryWidget(
userId: '1',
),
);
}
}

View file

@ -1,28 +0,0 @@
name: example
description: "A new Flutter project."
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.5.0
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
flutter_chat:
path: ../
# firebase_chat_repository:
# path: ../../firebase_chat_repository
# firebase_core: any
# firebase_auth: any
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
flutter:
uses-material-design: true

View file

@ -1,30 +0,0 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:example/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

View file

@ -1,31 +0,0 @@
// Core
export "package:chat_repository_interface/chat_repository_interface.dart";
// User story
export "package:flutter_chat/src/flutter_chat_entry_widget.dart";
export "package:flutter_chat/src/flutter_chat_navigator_userstories.dart";
// Options
export "src/config/chat_builders.dart";
export "src/config/chat_options.dart";
export "src/config/chat_time_indicator_options.dart";
export "src/config/chat_translations.dart";
export "src/config/screen_types.dart";
// Screens and widgets
export "src/screens/chat_detail/chat_detail_screen.dart";
export "src/screens/chat_detail/widgets/default_message_builder.dart";
export "src/screens/chat_detail/widgets/old_message_builder.dart";
export "src/screens/chat_profile_screen.dart";
export "src/screens/chat_screen.dart";
export "src/screens/creation/new_chat_screen.dart";
export "src/screens/creation/new_group_chat_overview.dart";
export "src/screens/creation/new_group_chat_screen.dart";
// Services
export "src/services/date_formatter.dart";
export "src/services/pop_handler.dart";
// Utils
export "src/util/scope.dart";
export "src/util/utils.dart";

View file

@ -1,204 +0,0 @@
import "dart:typed_data";
import "package:flutter/material.dart";
import "package:flutter_chat/flutter_chat.dart";
import "package:flutter_chat/src/screens/chat_detail/widgets/default_loader.dart";
import "package:flutter_chat/src/screens/creation/widgets/default_image_picker.dart";
/// The chat builders
class ChatBuilders {
/// The chat builders constructor
const ChatBuilders({
this.chatMessagesErrorBuilder,
this.baseScreenBuilder,
this.chatScreenBuilder,
this.messageInputBuilder,
this.chatRowContainerBuilder,
this.groupAvatarBuilder,
this.imagePickerContainerBuilder,
this.userAvatarBuilder,
this.deleteChatDialogBuilder,
this.newChatButtonBuilder,
this.noUsersPlaceholderBuilder,
this.chatTitleBuilder,
this.chatMessageBuilder = DefaultChatMessageBuilder.builder,
this.imagePickerBuilder = DefaultImagePickerDialog.builder,
this.usernameBuilder,
this.loadingWidgetBuilder = DefaultChatLoadingOverlay.builder,
this.loadingChatMessageBuilder = DefaultChatMessageLoader.builder,
});
/// The base screen builder
/// This builder is used to build the base screen for the chat
/// You can switch on the [screenType] to build different screens
/// ```dart
/// baseScreenBuilder: (context, screenType, appBar, body) {
/// switch (screenType) {
/// case ScreenType.chatScreen:
/// return Scaffold(
/// appBar: appBar,
/// body: body,
/// );
/// case ScreenType.chatDetailScreen:
/// // And so on....
/// ```
final BaseScreenBuilder? baseScreenBuilder;
/// The chat screen builder
/// This builder is used instead of the [baseScreenBuilder] when building the
/// chat screen. While the chat is still loading the [chat] will be null
final ChatScreenBuilder? chatScreenBuilder;
/// The message input builder
final TextInputBuilder? messageInputBuilder;
/// The chat row container builder
final ContainerBuilder? chatRowContainerBuilder;
/// The group avatar builder
final GroupAvatarBuilder? groupAvatarBuilder;
/// The user avatar builder
final UserAvatarBuilder? userAvatarBuilder;
/// The delete chat dialog builder
final Future<bool?> Function(BuildContext, ChatModel)?
deleteChatDialogBuilder;
/// The new chat button builder
final ButtonBuilder? newChatButtonBuilder;
/// The no users placeholder builder
final NoUsersPlaceholderBuilder? noUsersPlaceholderBuilder;
/// The chat title builder
final Widget Function(String chatTitle)? chatTitleBuilder;
/// The chat message builder
final ChatMessageBuilder chatMessageBuilder;
/// The username builder
final Widget Function(String userFullName)? usernameBuilder;
/// The image picker container builder
final ImagePickerContainerBuilder? imagePickerContainerBuilder;
/// A way to provide your own image picker implementation
/// If not provided the [DefaultImagePicker.builder] will be used which
/// shows a modal buttom sheet with the option for a camera or gallery image
final ImagePickerBuilder imagePickerBuilder;
/// The loading widget builder
/// This is used to build the loading widget that is displayed on the chat
/// screen when loading the chat
final WidgetBuilder loadingWidgetBuilder;
/// The loading widget builder for chat messages
/// This is displayed in the list of chat messages when loading more messages
/// can be above and below the list
final WidgetBuilder loadingChatMessageBuilder;
/// Errorbuilder for when messages are not loading correctly on the detail
/// screen of a chat.
final ChatErrorBuilder? chatMessagesErrorBuilder;
}
/// The button builder
typedef ButtonBuilder = Widget Function(
BuildContext context,
VoidCallback onPressed,
ChatTranslations translations,
);
/// The image picker container builder
typedef ImagePickerContainerBuilder = Widget Function(
BuildContext context,
VoidCallback onClose,
ChatTranslations translations,
);
/// Builder definition for providing an image picker implementation
typedef ImagePickerBuilder = Future<Uint8List?> Function(
BuildContext context,
);
/// The text input builder
typedef TextInputBuilder = Widget Function(
BuildContext context, {
required TextEditingController textEditingController,
required Widget suffixIcon,
required ChatTranslations translations,
required VoidCallback onSubmit,
required bool enabled,
});
/// The base screen builder
/// [title] is the title of the screen and can be null while loading
typedef BaseScreenBuilder = Widget Function(
BuildContext context,
ScreenType screenType,
PreferredSizeWidget appBar,
String? title,
Widget body,
);
/// The chat screen builder
typedef ChatScreenBuilder = Widget Function(
BuildContext context,
ChatModel? chat,
PreferredSizeWidget appBar,
String? title,
Widget body,
);
/// The container builder
typedef ContainerBuilder = Widget Function(
BuildContext context,
Widget child,
);
/// The chat message builder
/// This builder is used to override the default chat message widget
/// If null is returned, the default chat message widget will be used so you can
/// override for specific cases
/// [previousMessage] is the previous message in the chat
/// [sender] is the sender of the message and null if no user sent the message
typedef ChatMessageBuilder = Widget? Function(
BuildContext context,
MessageModel message,
MessageModel? previousMessage,
UserModel? sender,
Function(UserModel sender) onPressSender,
String semanticIdTitle,
String semanticIdText,
String semanticIdTime,
);
/// The group avatar builder
typedef GroupAvatarBuilder = Widget Function(
BuildContext context,
String groupName,
String? imageUrl,
double size,
);
/// The user avatar builder
typedef UserAvatarBuilder = Widget Function(
BuildContext context,
UserModel user,
double size,
);
/// The no users placeholder builder
typedef NoUsersPlaceholderBuilder = Widget Function(
BuildContext context,
ChatTranslations translations,
);
/// Builder for when there is an error on a chatscreen
typedef ChatErrorBuilder = Widget Function(
BuildContext context,
Object error,
StackTrace stackTrace,
ChatOptions options,
);

View file

@ -1,374 +0,0 @@
import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/material.dart";
import "package:flutter_chat/flutter_chat.dart";
import "package:flutter_chat/src/config/chat_semantics.dart";
/// The chat options
/// Use this class to configure the chat options.
class ChatOptions {
/// The chat options constructor
ChatOptions({
this.dateformat,
this.groupChatEnabled = true,
this.enableLoadingIndicator = true,
this.translations = const ChatTranslations.empty(),
this.semantics = const ChatSemantics.standard(),
this.builders = const ChatBuilders(),
this.spacing = const ChatSpacing(),
this.paginationControls = const ChatPaginationControls(),
this.messageTheme,
this.messageThemeResolver = _defaultMessageThemeResolver,
this.chatTitleResolver,
this.senderTitleResolver,
this.iconEnabledColor,
this.iconDisabledColor,
this.chatAlignment,
this.onNoChats,
this.imageQuality = 20,
this.imageProviderResolver = _defaultImageProviderResolver,
this.timeIndicatorOptions = const ChatTimeIndicatorOptions(),
ChatRepositoryInterface? chatRepository,
UserRepositoryInterface? userRepository,
PendingMessageRepositoryInterface? pendingMessagesRepository,
}) : chatRepository = chatRepository ?? LocalChatRepository(),
userRepository = userRepository ?? LocalUserRepository(),
pendingMessagesRepository =
pendingMessagesRepository ?? LocalPendingMessageRepository();
/// The implementation for communication with persistance layer for chats
final ChatRepositoryInterface chatRepository;
/// The implementation for communication with persistance layer
/// for pending messages
final PendingMessageRepositoryInterface pendingMessagesRepository;
/// The implementation for communication with persistance layer for users
final UserRepositoryInterface userRepository;
/// [dateformat] is a function that formats the date.
// ignore: avoid_positional_boolean_parameters
final String Function(bool showFullDate, DateTime date)? dateformat;
/// [translations] is the chat translations.
final ChatTranslations translations;
/// [semantics] is the chat semantics.
final ChatSemantics semantics;
/// [builders] is the chat builders.
final ChatBuilders builders;
//// The spacing between elements of the chat
final ChatSpacing spacing;
/// The pagination settings for the chat
final ChatPaginationControls paginationControls;
/// [groupChatEnabled] is a boolean that indicates if group chat is enabled.
final bool groupChatEnabled;
/// [iconEnabledColor] is the color of the enabled icon.
/// Defaults to the [IconThemeData.color] of the current [Theme]
final Color? iconEnabledColor;
/// [iconDisabledColor] is the color of the disabled icon.
/// Defaults to the [ThemeData.disabledColor] of the current [Theme]
final Color? iconDisabledColor;
/// The default [MessageTheme] for the chat messages.
/// If not set, the default values are based on the current [Theme].
final MessageTheme? messageTheme;
/// If [messageThemeResolver] is set and returns null for a message,
/// the [messageTheme] will be used.
final MessageThemeResolver messageThemeResolver;
/// If [chatTitleResolver] is set, it will be used to get the title of
/// the chat in the ChatDetailScreen.
final ChatTitleResolver? chatTitleResolver;
/// If [senderTitleResolver] is set, it will be used to get the title of
/// the sender in a chat message. If not set, the [sender.firstName] is used.
/// [sender] can be null if the message is an event.
final SenderTitleResolver? senderTitleResolver;
/// The alignment of the chatmessages in the ChatDetailScreen.
/// Defaults to [Alignment.bottomCenter]
final Alignment? chatAlignment;
/// Enable the loading indicator that is over the entire chat screen while
/// loading messages. Defaults to false. The streambuilder for chat messages
/// already shows a loading indicator. So this is an additional loading that
/// can be used for more customization.
final bool enableLoadingIndicator;
/// [onNoChats] is a function that is triggered when there are no chats.
final Function? onNoChats;
/// [imageQuality] sets the quality of the image to send over with chat image
/// messages. This should be a value between 1 and 100 where 1 is the worst
/// image quality and 100 is the best image quality. Note that the higher the
/// image quality is set, the larger te iage is, that is being sent over.
final int imageQuality;
/// If [imageProviderResolver] is set, it will be used to get the images for
/// the images in the entire userstory. If not provided, CachedNetworkImage
/// will be used.
final ImageProviderResolver imageProviderResolver;
/// Options regarding the time indicator in chat screens
final ChatTimeIndicatorOptions timeIndicatorOptions;
}
/// Typedef for the chatTitleResolver function that is used to get a title for
/// a chat.
typedef ChatTitleResolver = String? Function(ChatModel chat);
/// Typedef for the senderTitleResolver function that is used to get a title for
/// a sender.
typedef SenderTitleResolver = String? Function(UserModel? user);
/// Typedef for the imageProviderResolver function that is used to get images
/// for the userstory.
typedef ImageProviderResolver = ImageProvider Function(
BuildContext context,
Uri image,
);
/// Typedef for the messageThemeResolver function that is used to get a
/// [MessageTheme] for a message. This can return null so you can fall back to
/// default values for some messages.
typedef MessageThemeResolver = MessageTheme? Function(
BuildContext context,
MessageModel message,
MessageModel? previousMessage,
UserModel? sender,
);
/// The message theme
class MessageTheme {
/// The message theme constructor
const MessageTheme({
this.backgroundColor,
this.nameColor,
this.borderColor,
this.textColor,
this.timeTextColor,
this.imageBackgroundColor,
this.borderRadius,
this.messageAlignment,
this.messageSidePadding,
this.textAlignment,
this.showName,
this.showTime,
this.showFullDate,
});
/// Creates a [MessageTheme] from a [ThemeData]
factory MessageTheme.fromTheme(ThemeData theme) => MessageTheme(
backgroundColor: theme.colorScheme.primary,
nameColor: theme.colorScheme.onPrimary,
borderColor: theme.colorScheme.primary,
textColor: theme.colorScheme.onPrimary,
timeTextColor: theme.colorScheme.onPrimary,
imageBackgroundColor: theme.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(12),
textAlignment: TextAlign.start,
messageSidePadding: 144.0,
messageAlignment: null,
showName: null,
showTime: true,
showFullDate: null,
);
/// The alignment of the message in the chat
/// By default, the current user is aligned to the right and the other senders
/// are aligned to the left.
final TextAlign? messageAlignment;
/// The alignment of the text in the message
/// Defaults to [TextAlign.start]
final TextAlign? textAlignment;
/// The color of the message text
/// Defaults to [ThemeData.colorScheme.onPrimary]
final Color? textColor;
/// The color of the text displaying the time
/// Defaults to [ThemeData.colorScheme.onPrimary]
final Color? timeTextColor;
/// The color of the sender name
/// Defaults to [ThemeData.colorScheme.onPrimary]
final Color? nameColor;
/// The color of the message container background
/// Defaults to [ThemeData.colorScheme.primary]
final Color? backgroundColor;
/// The color of the border around the message
/// Defaults to [ThemeData.colorScheme.primaryColor]
final Color? borderColor;
/// The color of the background when an image is loading, the image is
/// transparent or there is an error.
///
/// Defaults to [ThemeData.colorScheme.secondaryContainer]
final Color? imageBackgroundColor;
/// The border radius of the message container
/// Defaults to [BorderRadius.circular(12)]
final BorderRadius? borderRadius;
/// The padding on the side of the message
/// If not set, the padding is 144.0
final double? messageSidePadding;
/// If the name of the sender should be shown above the message
/// If not set the name will be shown if the previous message was not from the
/// same sender.
final bool? showName;
/// If the time of the message should be shown below the message
/// Defaults to true
final bool? showTime;
/// If the full date should be shown with the time in the message
/// If not set the date will be shown if the previous message was not on the
/// same day.
/// If [showTime] is false, this value is ignored.
final bool? showFullDate;
/// Creates a copy of the current object with the provided values
MessageTheme copyWith({
Color? backgroundColor,
Color? nameColor,
Color? borderColor,
Color? textColor,
Color? timeTextColor,
Color? imageBackgroundColor,
BorderRadius? borderRadius,
double? messageSidePadding,
TextAlign? messageAlignment,
TextAlign? textAlignment,
bool? showName,
bool? showTime,
bool? showFullDate,
}) =>
MessageTheme(
backgroundColor: backgroundColor ?? this.backgroundColor,
nameColor: nameColor ?? this.nameColor,
borderColor: borderColor ?? this.borderColor,
textColor: textColor ?? this.textColor,
timeTextColor: timeTextColor ?? this.timeTextColor,
imageBackgroundColor: imageBackgroundColor ?? this.imageBackgroundColor,
borderRadius: borderRadius ?? this.borderRadius,
messageSidePadding: messageSidePadding ?? this.messageSidePadding,
messageAlignment: messageAlignment ?? this.messageAlignment,
textAlignment: textAlignment ?? this.textAlignment,
showName: showName ?? this.showName,
showTime: showTime ?? this.showTime,
showFullDate: showFullDate ?? this.showFullDate,
);
/// If a value is null in the first object, the value from the second object
/// is used.
MessageTheme operator |(MessageTheme other) => MessageTheme(
backgroundColor: backgroundColor ?? other.backgroundColor,
nameColor: nameColor ?? other.nameColor,
borderColor: borderColor ?? other.borderColor,
textColor: textColor ?? other.textColor,
timeTextColor: timeTextColor ?? other.timeTextColor,
imageBackgroundColor:
imageBackgroundColor ?? other.imageBackgroundColor,
borderRadius: borderRadius ?? other.borderRadius,
messageSidePadding: messageSidePadding ?? other.messageSidePadding,
messageAlignment: messageAlignment ?? other.messageAlignment,
textAlignment: textAlignment ?? other.textAlignment,
showName: showName ?? other.showName,
showTime: showTime ?? other.showTime,
showFullDate: showFullDate ?? other.showFullDate,
);
}
MessageTheme? _defaultMessageThemeResolver(
BuildContext context,
MessageModel message,
MessageModel? previousMessage,
UserModel? sender,
) =>
null;
ImageProvider _defaultImageProviderResolver(
BuildContext context,
Uri image,
) =>
switch (image.scheme) {
"data" => MemoryImage(image.data!.contentAsBytes()),
_ => CachedNetworkImageProvider(image.toString()),
};
/// All configurable paddings and whitespaces within the userstory
class ChatSpacing {
/// Creates a ChatSpacing object
const ChatSpacing({
this.chatBetweenMessagesPadding = 16.0,
this.chatSidePadding = 20.0,
});
/// The padding between the chat messages and the screen edge
final double chatSidePadding;
/// The padding between different chat messages if they are not from the same
/// sender.
final double chatBetweenMessagesPadding;
}
/// The chat pagination controls
/// Use this to define how sensitive the chat pagination should be.
class ChatPaginationControls {
/// The chat pagination controls constructor
const ChatPaginationControls({
this.scrollOffset = 50.0,
this.autoScrollTriggerOffset = 50.0,
this.loadingIndicatorForNewMessages = true,
this.loadingIndicatorForOldMessages = true,
this.loadNewMessagesOnScroll = true,
this.loadOldMessagesOnScroll = true,
this.loadingNewMessageMinDuration = Duration.zero,
this.loadingOldMessageMinDuration = Duration.zero,
});
/// The minimum scroll offset to trigger the pagination to call for more pages
/// on both sides of the chat. Defaults to 50.0
final double scrollOffset;
/// The minimum scroll offset to trigger the auto scroll to the bottom of the
/// chat. Defaults to 50.0
final double autoScrollTriggerOffset;
/// Whether to load new messages when someone scrolls to the end of the chat
final bool loadNewMessagesOnScroll;
/// Whether to load older messages when scrolling towards the start of the
/// chat.
///
/// If the messages are loaded by pagination, it is smart to add this.
final bool loadOldMessagesOnScroll;
/// Whether to show a loading indicator for new messages loading
final bool loadingIndicatorForNewMessages;
/// Whether to show a loading indicator for old messages loading
final bool loadingIndicatorForOldMessages;
/// The minimum duration for the loading indicator for new messages
/// to be shown. The loading indicator will wait for this duration and the
/// completion of [ChatService.loadNewMessagesAfter]
final Duration loadingNewMessageMinDuration;
/// The minimum duration for the loading indicator for old messages
/// to be shown. The loading indicator will wait for this duration and the
/// completion of [ChatService.loadOldMessagesBefore]
final Duration loadingOldMessageMinDuration;
}

View file

@ -1,271 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
// ignore_for_file: public_member_api_docs
/// Class that holds all the semantic ids for the chat component view and
/// the corresponding userstory
class ChatSemantics {
/// ChatSemantics constructor where everything is required use this
/// if you want to be sure to have all translations specified
/// If you just want the default values use the standard constructor
/// and optionally override the values with the copyWith method
const ChatSemantics({
required this.profileTitle,
required this.profileDescription,
required this.chatUnreadMessages,
required this.chatChatTitle,
required this.chatNoMessages,
required this.newChatGetUsersError,
required this.newGroupChatMemberAmount,
required this.newGroupChatGetUsersError,
required this.newChatUserListUserFullName,
required this.chatBubbleTitle,
required this.chatBubbleTime,
required this.chatBubbleText,
required this.chatsChatTitle,
required this.chatsChatSubTitle,
required this.chatsChatLastUsed,
required this.chatsChatUnreadMessages,
required this.chatMessageInput,
required this.newChatNameInput,
required this.newChatBioInput,
required this.newChatSearchInput,
required this.newGroupChatSearchInput,
required this.profileStartChatButton,
required this.chatsStartChatButton,
required this.chatsDeleteConfirmButton,
required this.newChatCreateGroupChatButton,
required this.newGroupChatCreateGroupChatButton,
required this.newGroupChatNextButton,
required this.imagePickerCancelButton,
required this.chatSelectImageIconButton,
required this.chatSendMessageIconButton,
required this.newChatSearchIconButton,
required this.newGroupChatSearchIconButton,
required this.chatBackButton,
required this.chatTitleButton,
required this.newGroupChatSelectImage,
required this.newGroupChatRemoveImage,
required this.newGroupChatRemoveUser,
required this.profileTapUserButton,
required this.chatsOpenChatButton,
required this.userListTapUser,
});
/// Default translations for the chat component view
const ChatSemantics.standard({
this.profileTitle = "text_profile_title",
this.profileDescription = "text_profile_description",
this.chatUnreadMessages = "text_unread_messages",
this.chatChatTitle = "text_chat_title",
this.chatNoMessages = "text_no_messages",
this.newChatGetUsersError = "text_get_users_error",
this.newGroupChatMemberAmount = "text_member_amount",
this.newGroupChatGetUsersError = "text_get_users_error",
this.newChatUserListUserFullName = _defaultNewChatUserListUserFullName,
this.chatBubbleTitle = _defaultChatBubbleTitle,
this.chatBubbleTime = _defaultChatBubbleTime,
this.chatBubbleText = _defaultChatBubbleText,
this.chatsChatTitle = _defaultChatsChatTitle,
this.chatsChatSubTitle = _defaultChatsChatSubTitle,
this.chatsChatLastUsed = _defaultChatsChatLastUsed,
this.chatsChatUnreadMessages = _defaultChatsChatUnreadMessages,
this.chatMessageInput = "input_text_message",
this.newChatNameInput = "input_text_name",
this.newChatBioInput = "input_text_bio",
this.newChatSearchInput = "input_text_search",
this.newGroupChatSearchInput = "input_text_search",
this.profileStartChatButton = "button_start_chat",
this.chatsStartChatButton = "button_start_chat",
this.chatsDeleteConfirmButton = "button_delete_chat_confirm",
this.newChatCreateGroupChatButton = "button_create_group_chat",
this.newGroupChatCreateGroupChatButton = "button_create_group_chat",
this.newGroupChatNextButton = "button_next",
this.imagePickerCancelButton = "button_cancel",
this.chatSelectImageIconButton = "button_icon_select_image",
this.chatSendMessageIconButton = "button_icon_send_message",
this.newChatSearchIconButton = "button_icon_search",
this.newGroupChatSearchIconButton = "button_icon_search",
this.chatBackButton = "button_back",
this.chatTitleButton = "button_open_profile",
this.newGroupChatSelectImage = "button_select_image",
this.newGroupChatRemoveImage = "button_remove_image",
this.newGroupChatRemoveUser = "button_remove_user",
this.profileTapUserButton = _defaultProfileTapUserButton,
this.chatsOpenChatButton = _defaultChatsOpenChatButton,
this.userListTapUser = _defaultUserListTapUser,
});
// Text
final String profileTitle;
final String profileDescription;
final String chatUnreadMessages;
final String chatChatTitle;
final String chatNoMessages;
final String newChatGetUsersError;
final String newGroupChatMemberAmount;
final String newGroupChatGetUsersError;
// Indexed text
final String Function(int index) newChatUserListUserFullName;
final String Function(int index) chatBubbleTitle;
final String Function(int index) chatBubbleTime;
final String Function(int index) chatBubbleText;
final String Function(int index) chatsChatTitle;
final String Function(int index) chatsChatSubTitle;
final String Function(int index) chatsChatLastUsed;
final String Function(int index) chatsChatUnreadMessages;
// Input texts
final String chatMessageInput;
final String newChatNameInput;
final String newChatBioInput;
final String newChatSearchInput;
final String newGroupChatSearchInput;
// Buttons
final String profileStartChatButton;
final String chatsStartChatButton;
final String chatsDeleteConfirmButton;
final String newChatCreateGroupChatButton;
final String newGroupChatCreateGroupChatButton;
final String newGroupChatNextButton;
final String imagePickerCancelButton;
// Icon buttons
final String chatSelectImageIconButton;
final String chatSendMessageIconButton;
final String newChatSearchIconButton;
final String newGroupChatSearchIconButton;
// Inkwells
final String chatBackButton;
final String chatTitleButton;
final String newGroupChatSelectImage;
final String newGroupChatRemoveImage;
final String newGroupChatRemoveUser;
// Indexed inkwells
final String Function(int index) profileTapUserButton;
final String Function(int index) chatsOpenChatButton;
final String Function(int index) userListTapUser;
ChatSemantics copyWith({
String? profileTitle,
String? profileDescription,
String? chatUnreadMessages,
String? chatChatTitle,
String? chatNoMessages,
String? newChatGetUsersError,
String? newGroupChatMemberAmount,
String? newGroupChatGetUsersError,
String Function(int)? newChatUserListUserFullName,
String Function(int)? chatBubbleTitle,
String Function(int)? chatBubbleTime,
String Function(int)? chatBubbleText,
String Function(int)? chatsChatTitle,
String Function(int)? chatsChatSubTitle,
String Function(int)? chatsChatLastUsed,
String Function(int)? chatsChatUnreadMessages,
String? chatMessageInput,
String? newChatNameInput,
String? newChatBioInput,
String? newChatSearchInput,
String? newGroupChatSearchInput,
String? profileStartChatButton,
String? chatsStartChatButton,
String? chatsDeleteConfirmButton,
String? newChatCreateGroupChatButton,
String? newGroupChatCreateGroupChatButton,
String? newGroupChatNextButton,
String? imagePickerCancelButton,
String? chatSelectImageIconButton,
String? chatSendMessageIconButton,
String? newChatSearchIconButton,
String? newGroupChatSearchIconButton,
String? chatBackButton,
String? chatTitleButton,
String? newGroupChatSelectImage,
String? newGroupChatRemoveImage,
String? newGroupChatRemoveUser,
String Function(int)? profileTapUserButton,
String Function(int)? chatsOpenChatButton,
String Function(int)? userListTapUser,
}) =>
ChatSemantics(
profileTitle: profileTitle ?? this.profileTitle,
profileDescription: profileDescription ?? this.profileDescription,
chatUnreadMessages: chatUnreadMessages ?? this.chatUnreadMessages,
chatChatTitle: chatChatTitle ?? this.chatChatTitle,
chatNoMessages: chatNoMessages ?? this.chatNoMessages,
newChatGetUsersError: newChatGetUsersError ?? this.newChatGetUsersError,
newGroupChatMemberAmount:
newGroupChatMemberAmount ?? this.newGroupChatMemberAmount,
newGroupChatGetUsersError:
newGroupChatGetUsersError ?? this.newGroupChatGetUsersError,
newChatUserListUserFullName:
newChatUserListUserFullName ?? this.newChatUserListUserFullName,
chatBubbleTitle: chatBubbleTitle ?? this.chatBubbleTitle,
chatBubbleTime: chatBubbleTime ?? this.chatBubbleTime,
chatBubbleText: chatBubbleText ?? this.chatBubbleText,
chatsChatTitle: chatsChatTitle ?? this.chatsChatTitle,
chatsChatSubTitle: chatsChatSubTitle ?? this.chatsChatSubTitle,
chatsChatLastUsed: chatsChatLastUsed ?? this.chatsChatLastUsed,
chatsChatUnreadMessages:
chatsChatUnreadMessages ?? this.chatsChatUnreadMessages,
chatMessageInput: chatMessageInput ?? this.chatMessageInput,
newChatNameInput: newChatNameInput ?? this.newChatNameInput,
newChatBioInput: newChatBioInput ?? this.newChatBioInput,
newChatSearchInput: newChatSearchInput ?? this.newChatSearchInput,
newGroupChatSearchInput:
newGroupChatSearchInput ?? this.newGroupChatSearchInput,
profileStartChatButton:
profileStartChatButton ?? this.profileStartChatButton,
chatsStartChatButton: chatsStartChatButton ?? this.chatsStartChatButton,
chatsDeleteConfirmButton:
chatsDeleteConfirmButton ?? this.chatsDeleteConfirmButton,
newChatCreateGroupChatButton:
newChatCreateGroupChatButton ?? this.newChatCreateGroupChatButton,
newGroupChatCreateGroupChatButton: newGroupChatCreateGroupChatButton ??
this.newGroupChatCreateGroupChatButton,
newGroupChatNextButton:
newGroupChatNextButton ?? this.newGroupChatNextButton,
imagePickerCancelButton:
imagePickerCancelButton ?? this.imagePickerCancelButton,
chatSelectImageIconButton:
chatSelectImageIconButton ?? this.chatSelectImageIconButton,
chatSendMessageIconButton:
chatSendMessageIconButton ?? this.chatSendMessageIconButton,
newChatSearchIconButton:
newChatSearchIconButton ?? this.newChatSearchIconButton,
newGroupChatSearchIconButton:
newGroupChatSearchIconButton ?? this.newGroupChatSearchIconButton,
chatBackButton: chatBackButton ?? this.chatBackButton,
chatTitleButton: chatTitleButton ?? this.chatTitleButton,
newGroupChatSelectImage:
newGroupChatSelectImage ?? this.newGroupChatSelectImage,
newGroupChatRemoveImage:
newGroupChatRemoveImage ?? this.newGroupChatRemoveImage,
newGroupChatRemoveUser:
newGroupChatRemoveUser ?? this.newGroupChatRemoveUser,
profileTapUserButton: profileTapUserButton ?? this.profileTapUserButton,
chatsOpenChatButton: chatsOpenChatButton ?? this.chatsOpenChatButton,
userListTapUser: userListTapUser ?? this.userListTapUser,
);
}
String _defaultNewChatUserListUserFullName(int index) =>
"text_user_fullname_$index";
String _defaultChatBubbleTitle(int index) => "text_chat_bubble_title_$index";
String _defaultChatBubbleTime(int index) => "text_chat_bubble_time_$index";
String _defaultChatBubbleText(int index) => "text_chat_bubble_text_$index";
String _defaultChatsChatTitle(int index) => "text_chat_title_$index";
String _defaultChatsChatSubTitle(int index) => "text_chat_sub_title_$index";
String _defaultChatsChatLastUsed(int index) => "text_chat_last_used_$index";
String _defaultChatsChatUnreadMessages(int index) =>
"text_chat_unread_messages_$index";
String _defaultProfileTapUserButton(int index) => "button_tap_user_$index";
String _defaultChatsOpenChatButton(int index) => "button_open_chat_$index";
String _defaultUserListTapUser(int index) => "button_tap_user_$index";

View file

@ -1,105 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_chat/flutter_chat.dart";
export "package:flutter_chat/src/screens/chat_detail/widgets/default_chat_time_indicator.dart";
/// All options related to the time indicator
class ChatTimeIndicatorOptions {
/// Create default ChatTimeIndicator options
const ChatTimeIndicatorOptions({
this.indicatorBuilder = DefaultChatTimeIndicator.builder,
this.labelResolver = defaultChatTimeIndicatorLabelResolver,
this.sectionCheck = defaultChatTimeIndicatorSectionChecker,
});
/// This completely disables the chat time indicator feature
const ChatTimeIndicatorOptions.none()
: indicatorBuilder = DefaultChatTimeIndicator.builder,
labelResolver = defaultChatTimeIndicatorLabelResolver,
sectionCheck = neverShowChatTimeIndicatorSectionChecker;
/// The general builder for the indicator
final ChatTimeIndicatorBuilder indicatorBuilder;
/// A function that translates offset / time to a string label
final ChatTimeIndicatorLabelResolver labelResolver;
/// A function that determines when a new section starts
///
/// By default, all messages are prefixed with a message.
/// You can disable this using the [skipFirstChatTimeIndicatorSectionChecker]
/// instead of the default, which would skip the first section
final ChatTimeIndicatorSectionChecker sectionCheck;
/// public method on the options for readability
bool isMessageInNewTimeSection(
BuildContext context,
MessageModel? previousMessage,
MessageModel currentMessage,
) =>
sectionCheck(
context,
previousMessage,
currentMessage,
);
}
/// A function that would generate a string given the current window/datetime
typedef ChatTimeIndicatorLabelResolver = String Function(
BuildContext context,
int dayOffset,
DateTime currentWindow,
);
/// A function that would determine if a chat indicator has to render
typedef ChatTimeIndicatorSectionChecker = bool Function(
BuildContext context,
MessageModel? previousMessage,
MessageModel currentMessage,
);
/// Build used to render time indicators on chat detail screens
typedef ChatTimeIndicatorBuilder = Widget Function(
BuildContext context,
String timeLabel,
);
///
String defaultChatTimeIndicatorLabelResolver(
BuildContext context,
int dayOffset,
DateTime currentWindow,
) {
var translations = ChatScope.of(context).options.translations;
return translations.chatTimeIndicatorLabel(dayOffset, currentWindow);
}
/// A function that disables the time indicator in chat
bool neverShowChatTimeIndicatorSectionChecker(
BuildContext context,
MessageModel? previousMessage,
MessageModel currentMessage,
) =>
false;
/// Variant of the default implementation for determining if a new section
/// starts, where the first section is skipped.
///
/// Renders a new indicator every new section, skipping the first section
bool skipFirstChatTimeIndicatorSectionChecker(
BuildContext context,
MessageModel? previousMessage,
MessageModel currentMessage,
) =>
previousMessage != null &&
previousMessage.timestamp.date.isBefore(currentMessage.timestamp.date);
/// Default implementation for determining if a new section starts.
///
/// Renders a new indicator every new section
bool defaultChatTimeIndicatorSectionChecker(
BuildContext context,
MessageModel? previousMessage,
MessageModel currentMessage,
) =>
previousMessage == null ||
previousMessage.timestamp.date.isBefore(currentMessage.timestamp.date);

View file

@ -1,273 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
// ignore_for_file: public_member_api_docs
import "package:intl/intl.dart";
/// Class that holds all the translations for the chat component view and
/// the corresponding userstory
class ChatTranslations {
/// ChatTranslations constructor where everything is required use this
/// if you want to be sure to have all translations specified
/// If you just want the default values use the empty constructor
/// and optionally override the values with the copyWith method
const ChatTranslations({
required this.chatsTitle,
required this.chatsUnread,
required this.newChatButton,
required this.newGroupChatButton,
required this.newChatTitle,
required this.image,
required this.searchPlaceholder,
required this.startTyping,
required this.cancelImagePickerBtn,
required this.messagePlaceholder,
required this.writeMessageToStartChat,
required this.writeFirstMessageInGroupChat,
required this.imageUploading,
required this.deleteChatModalTitle,
required this.deleteChatModalDescription,
required this.deleteChatModalCancel,
required this.deleteChatModalConfirm,
required this.noUsersFound,
required this.noChatsFound,
required this.chatProfileUsers,
required this.imagePickerTitle,
required this.uploadFile,
required this.takePicture,
required this.anonymousUser,
required this.groupNameValidatorEmpty,
required this.groupNameValidatorTooLong,
required this.groupNameHintText,
required this.newGroupChatTitle,
required this.groupBioHintText,
required this.groupProfileBioHeader,
required this.groupBioValidatorEmpty,
required this.groupChatNameFieldHeader,
required this.groupBioFieldHeader,
required this.selectedMembersHeader,
required this.createGroupChatButton,
required this.groupNameEmpty,
required this.messagesLoadingError,
required this.next,
required this.chatTimeIndicatorLabel,
});
/// Default translations for the chat component view
const ChatTranslations.empty({
this.chatsTitle = "Chats",
this.chatsUnread = "unread",
this.newChatButton = "Start chat",
this.newGroupChatButton = "Start a groupchat",
this.newChatTitle = "Start a chat",
this.image = "Image",
this.searchPlaceholder = "Search...",
this.startTyping = "Start typing to find a user to chat with",
this.cancelImagePickerBtn = "Cancel",
this.messagePlaceholder = "Write your message here...",
this.writeMessageToStartChat = "Write a message to start the chat",
this.writeFirstMessageInGroupChat =
"Write the first message in this group chat",
this.imageUploading = "Image is uploading...",
this.deleteChatModalTitle = "Delete chat",
this.deleteChatModalDescription =
"Are you sure you want to delete this chat?",
this.deleteChatModalCancel = "Cancel",
this.deleteChatModalConfirm = "Confirm",
this.noUsersFound = "No users were found to start a chat with",
this.noChatsFound = "Click on 'Start a chat' to create a new chat",
this.anonymousUser = "Anonymous user",
this.chatProfileUsers = "Members:",
this.imagePickerTitle = "Do you want to upload a file or take a picture?",
this.uploadFile = "UPLOAD FILE",
this.takePicture = "TAKE PICTURE",
this.groupNameHintText = "Groupchat name",
this.groupNameValidatorEmpty = "Please enter a group chat name",
this.groupNameValidatorTooLong =
"Group name is too long, max 15 characters",
this.newGroupChatTitle = "start a groupchat",
this.groupBioHintText = "Bio",
this.groupProfileBioHeader = "Bio",
this.groupBioValidatorEmpty = "Please enter a bio",
this.groupChatNameFieldHeader = "Chat name",
this.groupBioFieldHeader = "Additional information for members",
this.selectedMembersHeader = "Members: ",
this.createGroupChatButton = "Create groupchat",
this.groupNameEmpty = "Group",
this.messagesLoadingError = "Error loading messages, you can reload below:",
this.next = "Next",
this.chatTimeIndicatorLabel =
ChatTranslations.defaultChatTimeIndicatorLabel,
});
final String chatsTitle;
final String chatsUnread;
final String newChatButton;
final String newGroupChatButton;
final String newChatTitle;
final String image;
final String searchPlaceholder;
final String startTyping;
final String cancelImagePickerBtn;
final String messagePlaceholder;
final String writeMessageToStartChat;
final String writeFirstMessageInGroupChat;
final String imageUploading;
final String deleteChatModalTitle;
final String deleteChatModalDescription;
final String deleteChatModalCancel;
final String deleteChatModalConfirm;
final String noUsersFound;
final String noChatsFound;
final String chatProfileUsers;
final String imagePickerTitle;
final String uploadFile;
final String takePicture;
final String groupChatNameFieldHeader;
final String groupBioFieldHeader;
final String selectedMembersHeader;
final String createGroupChatButton;
/// Shown when the user has no name
final String anonymousUser;
final String groupNameValidatorEmpty;
final String groupNameValidatorTooLong;
final String groupNameHintText;
final String newGroupChatTitle;
final String groupBioHintText;
final String groupProfileBioHeader;
final String groupBioValidatorEmpty;
final String groupNameEmpty;
/// message shown in the default chat screen when the chat messages are unable
/// to be loaded.
final String messagesLoadingError;
/// The message of a label given a certain offset.
///
/// The offset determines whether it is today (0), yesterday (-1), or earlier.
///
/// [dateOffset] will rarely be a +1, however if anyone ever wants to see
/// future chat messages, then this number will be positive.
///
/// use the given [time] format to display exact time information.
final String Function(int dateOffset, DateTime time) chatTimeIndicatorLabel;
/// Standard function to convert an offset to a String.
///
/// Recommended to always override this in any production app with an
/// app localizations implementation.
static String defaultChatTimeIndicatorLabel(
int dateOffset,
DateTime time,
) =>
switch (dateOffset) {
0 => "Today",
-1 => "Yesterday",
1 => "Tomorrow",
int value when value < 5 && value > 1 => "In $value days",
int value when value < -1 && value > -5 => "${value.abs()} days ago",
_ => DateFormat("dd-MM-YYYY").format(time),
};
final String next;
// copyWith method to override the default values
ChatTranslations copyWith({
String? chatsTitle,
String? chatsUnread,
String? newChatButton,
String? newGroupChatButton,
String? newChatTitle,
String? image,
String? searchPlaceholder,
String? startTyping,
String? cancelImagePickerBtn,
String? messagePlaceholder,
String? writeMessageToStartChat,
String? writeFirstMessageInGroupChat,
String? imageUploading,
String? deleteChatModalTitle,
String? deleteChatModalDescription,
String? deleteChatModalCancel,
String? deleteChatModalConfirm,
String? noUsersFound,
String? noChatsFound,
String? chatProfileUsers,
String? imagePickerTitle,
String? uploadFile,
String? takePicture,
String? anonymousUser,
String? groupNameValidatorEmpty,
String? groupNameValidatorTooLong,
String? groupNameHintText,
String? newGroupChatTitle,
String? groupBioHintText,
String? groupProfileBioHeader,
String? groupBioValidatorEmpty,
String? groupChatNameFieldHeader,
String? groupBioFieldHeader,
String? selectedMembersHeader,
String? createGroupChatButton,
String? groupNameEmpty,
String? messagesLoadingError,
String? next,
String Function(int dateOffset, DateTime time)? chatTimeIndicatorLabel,
}) =>
ChatTranslations(
chatsTitle: chatsTitle ?? this.chatsTitle,
chatsUnread: chatsUnread ?? this.chatsUnread,
newChatButton: newChatButton ?? this.newChatButton,
newGroupChatButton: newGroupChatButton ?? this.newGroupChatButton,
newChatTitle: newChatTitle ?? this.newChatTitle,
image: image ?? this.image,
searchPlaceholder: searchPlaceholder ?? this.searchPlaceholder,
startTyping: startTyping ?? this.startTyping,
cancelImagePickerBtn: cancelImagePickerBtn ?? this.cancelImagePickerBtn,
messagePlaceholder: messagePlaceholder ?? this.messagePlaceholder,
writeMessageToStartChat:
writeMessageToStartChat ?? this.writeMessageToStartChat,
writeFirstMessageInGroupChat:
writeFirstMessageInGroupChat ?? this.writeFirstMessageInGroupChat,
imageUploading: imageUploading ?? this.imageUploading,
deleteChatModalTitle: deleteChatModalTitle ?? this.deleteChatModalTitle,
deleteChatModalDescription:
deleteChatModalDescription ?? this.deleteChatModalDescription,
deleteChatModalCancel:
deleteChatModalCancel ?? this.deleteChatModalCancel,
deleteChatModalConfirm:
deleteChatModalConfirm ?? this.deleteChatModalConfirm,
noUsersFound: noUsersFound ?? this.noUsersFound,
noChatsFound: noChatsFound ?? this.noChatsFound,
chatProfileUsers: chatProfileUsers ?? this.chatProfileUsers,
imagePickerTitle: imagePickerTitle ?? this.imagePickerTitle,
uploadFile: uploadFile ?? this.uploadFile,
takePicture: takePicture ?? this.takePicture,
anonymousUser: anonymousUser ?? this.anonymousUser,
groupNameValidatorEmpty:
groupNameValidatorEmpty ?? this.groupNameValidatorEmpty,
groupNameValidatorTooLong:
groupNameValidatorTooLong ?? this.groupNameValidatorTooLong,
groupNameHintText: groupNameHintText ?? this.groupNameHintText,
newGroupChatTitle: newGroupChatTitle ?? this.newGroupChatTitle,
groupBioHintText: groupBioHintText ?? this.groupBioHintText,
groupProfileBioHeader:
groupProfileBioHeader ?? this.groupProfileBioHeader,
groupBioValidatorEmpty:
groupBioValidatorEmpty ?? this.groupBioValidatorEmpty,
groupChatNameFieldHeader:
groupChatNameFieldHeader ?? this.groupChatNameFieldHeader,
groupBioFieldHeader: groupBioFieldHeader ?? this.groupBioFieldHeader,
selectedMembersHeader:
selectedMembersHeader ?? this.selectedMembersHeader,
createGroupChatButton:
createGroupChatButton ?? this.createGroupChatButton,
groupNameEmpty: groupNameEmpty ?? this.groupNameEmpty,
messagesLoadingError: messagesLoadingError ?? this.messagesLoadingError,
next: next ?? this.next,
chatTimeIndicatorLabel:
chatTimeIndicatorLabel ?? this.chatTimeIndicatorLabel,
);
}

View file

@ -1,41 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_chat/src/screens/chat_detail/chat_detail_screen.dart";
import "package:flutter_chat/src/screens/chat_profile_screen.dart";
import "package:flutter_chat/src/screens/chat_screen.dart";
import "package:flutter_chat/src/screens/creation/new_chat_screen.dart";
import "package:flutter_chat/src/screens/creation/new_group_chat_overview.dart";
import "package:flutter_chat/src/screens/creation/new_group_chat_screen.dart";
/// Type of screen, used in custom screen builders
enum ScreenType {
/// Screen displaying an overview of chats
chatScreen(screen: ChatScreen),
/// Screen displaying a single chat
chatDetailScreen(screen: ChatDetailScreen),
/// Screen displaying the profile of a user within a chat
chatProfileScreen(screen: ChatProfileScreen),
/// Screen with a form to create a new chat
newChatScreen(screen: NewChatScreen),
/// Screen with a form to create a new group chat
newGroupChatScreen(screen: NewGroupChatScreen),
/// Screen displaying all group chats
newGroupChatOverview(screen: NewGroupChatOverview);
const ScreenType({
required Type screen,
}) : _screen = screen;
final Type _screen;
}
/// Extension for mapping widgets to [ScreenType]s
extension MapFromWidget on Widget {
/// returns corresponding [ScreenType]
ScreenType get mapScreenType =>
ScreenType.values.firstWhere((e) => e._screen == runtimeType);
}

View file

@ -1,218 +0,0 @@
import "dart:async";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/flutter_chat.dart";
/// A widget representing an entry point for a chat UI.
class FlutterChatEntryWidget extends StatefulWidget {
/// Constructs a [FlutterChatEntryWidget].
const FlutterChatEntryWidget({
required this.userId,
this.options,
this.onTap,
this.widgetSize = 75,
this.backgroundColor = Colors.grey,
this.icon = Icons.chat,
this.iconColor = Colors.black,
this.counterBackgroundColor = Colors.red,
this.textStyle,
this.semanticIdUnreadMessages = "text_unread_messages_count",
this.semanticIdOpenButton = "button_open_chat",
super.key,
});
/// The user ID of the person currently looking at the chat
final String userId;
/// Background color of the widget.
final Color backgroundColor;
/// Size of the widget.
final double widgetSize;
/// Background color of the counter.
final Color counterBackgroundColor;
/// Callback function triggered when the widget is tapped.
final Function()? onTap;
/// Icon to be displayed.
final IconData icon;
/// Color of the icon.
final Color iconColor;
/// Text style for the counter.
final TextStyle? textStyle;
/// The chat options
final ChatOptions? options;
/// Semantic Id for the unread messages text
final String semanticIdUnreadMessages;
/// Semantic Id for the unread messages text
final String semanticIdOpenButton;
@override
State<FlutterChatEntryWidget> createState() => _FlutterChatEntryWidgetState();
}
/// State class for [FlutterChatEntryWidget].
class _FlutterChatEntryWidgetState extends State<FlutterChatEntryWidget> {
late ChatService chatService;
@override
void initState() {
super.initState();
_initChatService();
}
@override
void didUpdateWidget(covariant FlutterChatEntryWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.userId != widget.userId ||
oldWidget.options != widget.options) {
_initChatService();
}
}
void _initChatService() {
chatService = ChatService(
userId: widget.userId,
chatRepository: widget.options?.chatRepository,
userRepository: widget.options?.userRepository,
pendingMessageRepository: widget.options?.pendingMessagesRepository,
);
}
@override
Widget build(BuildContext context) => CustomSemantics(
identifier: widget.semanticIdOpenButton,
buttonWithVariableText: true,
child: InkWell(
onTap: () async =>
widget.onTap?.call() ??
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => FlutterChatNavigatorUserstory(
userId: widget.userId,
options: widget.options ?? ChatOptions(),
),
),
),
child: StreamBuilder<int>(
stream: chatService.getUnreadMessagesCount(),
builder: (BuildContext context, snapshot) => 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: CustomSemantics(
identifier: widget.semanticIdUnreadMessages,
value: snapshot.data?.toString() ?? "0",
child: Text(
snapshot.data?.toString() ?? "0",
style: widget.textStyle,
),
),
),
),
),
],
),
),
),
);
}
/// Stateful widget representing an animated notification icon.
class _AnimatedNotificationIcon extends StatefulWidget {
const _AnimatedNotificationIcon({
required this.notifications,
required this.icon,
});
/// The number of notifications.
final int notifications;
/// The icon to be displayed.
final Icon icon;
@override
State<_AnimatedNotificationIcon> createState() =>
_AnimatedNotificationIconState();
}
/// State class for [_AnimatedNotificationIcon].
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() {
_animationController.dispose();
super.dispose();
}
@override
void didUpdateWidget(covariant _AnimatedNotificationIcon oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.notifications != widget.notifications) {
unawaited(_runAnimation());
}
}
Future<void> _runAnimation() async {
await _animationController.forward();
await _animationController.reverse();
}
@override
Widget build(BuildContext context) => RotationTransition(
turns: Tween(begin: 0.0, end: -.1)
.chain(CurveTween(curve: Curves.elasticIn))
.animate(_animationController),
child: widget.icon,
);
}

View file

@ -1,121 +0,0 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_chat/src/config/chat_options.dart";
import "package:flutter_chat/src/routes.dart";
import "package:flutter_chat/src/services/pop_handler.dart";
import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_hooks/flutter_hooks.dart";
/// Default Chat Userstory that starts at the chat list screen.
class FlutterChatNavigatorUserstory extends _BaseChatNavigatorUserstory {
/// Constructs a [FlutterChatNavigatorUserstory].
const FlutterChatNavigatorUserstory({
required super.userId,
required super.options,
super.onExit,
super.key,
});
@override
MaterialPageRoute buildInitialRoute(
BuildContext context,
ChatService service,
PopHandler popHandler,
) =>
chatOverviewRoute(
userId: userId,
chatService: service,
onExit: onExit,
);
}
/// Chat Userstory that starts directly in a chat detail screen.
class FlutterChatDetailNavigatorUserstory extends _BaseChatNavigatorUserstory {
/// Constructs a [FlutterChatDetailNavigatorUserstory].
const FlutterChatDetailNavigatorUserstory({
required super.userId,
required super.options,
required this.chatId,
super.onExit,
super.key,
});
/// The identifier of the chat to start in.
/// The [ChatModel] will be fetched from the [ChatRepository]
final String chatId;
@override
MaterialPageRoute buildInitialRoute(
BuildContext context,
ChatService service,
PopHandler popHandler,
) =>
chatDetailRoute(
chatId: chatId,
userId: userId,
chatService: service,
onExit: onExit,
);
}
/// Base hook widget for chat navigator userstories.
abstract class _BaseChatNavigatorUserstory extends HookWidget {
/// Constructs a [_BaseChatNavigatorUserstory].
const _BaseChatNavigatorUserstory({
required this.userId,
required this.options,
this.onExit,
super.key,
});
/// The user ID of the person starting the chat userstory.
final String userId;
/// The chat userstory configuration.
final ChatOptions options;
/// Callback for when the user wants to navigate back.
final VoidCallback? onExit;
/// Implemented by subclasses to provide the initial route of the userstory.
MaterialPageRoute buildInitialRoute(
BuildContext context,
ChatService service,
PopHandler popHandler,
);
@override
Widget build(BuildContext context) {
var service = useMemoized(
() => ChatService(
userId: userId,
chatRepository: options.chatRepository,
userRepository: options.userRepository,
pendingMessageRepository: options.pendingMessagesRepository,
),
[userId, options],
);
var popHandler = useMemoized(PopHandler.new, []);
var nestedNavigatorKey = useMemoized(GlobalKey<NavigatorState>.new, []);
return ChatScope(
userId: userId,
options: options,
service: service,
popHandler: popHandler,
child: NavigatorPopHandler(
onPop: () => popHandler.handlePop(),
child: Navigator(
key: nestedNavigatorKey,
onGenerateInitialRoutes: (_, __) => [
buildInitialRoute(context, service, popHandler),
],
),
),
);
}
}

View file

@ -1,323 +0,0 @@
import "dart:async";
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_chat/src/screens/chat_detail/chat_detail_screen.dart";
import "package:flutter_chat/src/screens/chat_profile_screen.dart";
import "package:flutter_chat/src/screens/chat_screen.dart";
import "package:flutter_chat/src/screens/creation/new_chat_screen.dart";
import "package:flutter_chat/src/screens/creation/new_group_chat_overview.dart";
import "package:flutter_chat/src/screens/creation/new_group_chat_screen.dart";
/// Pushes the chat overview screen
MaterialPageRoute chatOverviewRoute({
required String userId,
required ChatService chatService,
required VoidCallback? onExit,
}) =>
MaterialPageRoute(
builder: (context) => ChatScreen(
onExit: onExit,
onPressChat: (chat) async => _routeToScreen(
context,
chatDetailRoute(
chatId: chat.id,
userId: userId,
chatService: chatService,
onExit: () => Navigator.of(context).pop(),
).builder(context),
),
onDeleteChat: (chat) async => chatService.deleteChat(chatId: chat.id),
onPressStartChat: () async => _routeToScreen(
context,
_newChatRoute(
userId: userId,
chatService: chatService,
).builder(context),
),
),
);
/// Pushes the chat detail screen
MaterialPageRoute chatDetailRoute({
required String chatId,
required String userId,
required ChatService chatService,
required VoidCallback? onExit,
}) =>
MaterialPageRoute(
builder: (context) => ChatDetailScreen(
chatId: chatId,
onExit: onExit,
onReadChat: (chat) async => chatService.markAsRead(chatId: chat.id),
onUploadImage: (data) async => chatService.sendImageMessage(
chatId: chatId,
userId: userId,
data: data,
),
onMessageSubmit: (text) async => chatService.sendMessage(
chatId: chatId,
senderId: userId,
text: text,
),
onPressChatTitle: (chat) async {
if (chat.isGroupChat) {
await _routeToScreen(
context,
_chatProfileRoute(
userId: userId,
chatService: chatService,
chat: chat,
onExit: () => Navigator.of(context).pop(),
).builder(context),
);
} else {
var otherUserId = chat.getOtherUser(userId);
var otherUser =
await chatService.getUser(userId: otherUserId).first;
if (!context.mounted) return;
await _routeToScreen(
context,
_chatProfileRoute(
userId: userId,
chatService: chatService,
user: otherUser,
onExit: () => Navigator.of(context).pop(),
).builder(context),
);
}
},
onPressUserProfile: (user) async => _routeToScreen(
context,
_chatProfileRoute(
userId: userId,
chatService: chatService,
user: user,
onExit: () => Navigator.of(context).pop(),
).builder(context),
),
),
);
MaterialPageRoute _chatProfileRoute({
required String userId,
required ChatService chatService,
required VoidCallback onExit,
UserModel? user,
ChatModel? chat,
}) =>
MaterialPageRoute(
builder: (context) => ChatProfileScreen(
userModel: user,
chatModel: chat,
onExit: onExit,
onTapUser: (userId) async {
var user = await chatService.getUser(userId: userId).first;
if (!context.mounted) return;
await _routeToScreen(
context,
_chatProfileRoute(
userId: userId,
chatService: chatService,
user: user,
onExit: () => Navigator.of(context).pop(),
).builder(context),
);
},
onPressStartChat: (userId) async {
var chat = await _createChat(userId, chatService, userId);
if (!context.mounted) return;
await _routeToScreen(
context,
chatDetailRoute(
chatId: chat.id,
userId: userId,
chatService: chatService,
onExit: () => Navigator.of(context).pop(),
).builder(context),
);
},
),
);
MaterialPageRoute _newChatRoute({
required String userId,
required ChatService chatService,
}) =>
MaterialPageRoute(
builder: (context) => NewChatScreen(
onExit: () => Navigator.of(context).pop(),
onPressCreateGroupChat: () async => _routeToScreen(
context,
_newGroupChatRoute(
userId: userId,
chatService: chatService,
).builder(context),
),
onPressCreateChat: (user) async {
var chat = await _createChat(user.id, chatService, userId);
if (!context.mounted) return;
await _replaceCurrentScreen(
context,
chatDetailRoute(
chatId: chat.id,
userId: userId,
chatService: chatService,
onExit: () => Navigator.of(context).pop(),
).builder(context),
);
},
),
);
MaterialPageRoute _newGroupChatRoute({
required String userId,
required ChatService chatService,
}) =>
MaterialPageRoute(
builder: (context) => NewGroupChatScreen(
onExit: () => Navigator.of(context).pop(),
onContinue: (users) async => _replaceCurrentScreen(
context,
_newGroupChatOverviewRoute(
userId: userId,
chatService: chatService,
users: users,
).builder(context),
),
),
);
MaterialPageRoute _newGroupChatOverviewRoute({
required String userId,
required ChatService chatService,
required List<UserModel> users,
}) =>
MaterialPageRoute(
builder: (context) => NewGroupChatOverview(
users: users,
onExit: () => Navigator.of(context).pop(),
onComplete: (users, title, description, image) async {
String? path;
if (image != null) {
path = await chatService.uploadImage(
path: "groups/$title",
image: image,
chatId: "",
);
}
var chat = await _createGroupChat(
users,
title,
description,
path,
chatService,
userId,
);
if (!context.mounted) return;
await _replaceCurrentScreen(
context,
chatDetailRoute(
chatId: chat.id,
userId: userId,
chatService: chatService,
onExit: () => Navigator.of(context).pop(),
).builder(context),
);
},
),
);
/// Helper function to create a chat
Future<ChatModel> _createChat(
String otherUserId,
ChatService chatService,
String userId,
) async {
ChatModel? chat;
try {
chat = await chatService.getChatByUser(
currentUser: userId,
otherUser: otherUserId,
);
} on Exception catch (_) {
chat = null;
}
if (chat == null) {
await chatService.createChat(
isGroupChat: false,
users: [
await chatService.getUser(userId: userId).first,
await chatService.getUser(userId: otherUserId).first,
],
);
chat = await chatService.getChatByUser(
currentUser: userId,
otherUser: otherUserId,
);
if (chat == null) throw Exception("Chat not created");
}
return chat;
}
/// Helper function to create a group chat
Future<ChatModel> _createGroupChat(
List<UserModel> userModels,
String title,
String description,
String? imageUrl,
ChatService chatService,
String userId,
) async {
ChatModel? chat;
try {
chat = await chatService.getGroupChatByUser(
currentUser: userId,
otherUsers: userModels,
chatName: title,
description: description,
);
} on Exception catch (_) {
chat = null;
}
if (chat == null) {
var currentUser = await chatService.getUser(userId: userId).first;
var otherUsers = await Future.wait(
userModels.map((e) => chatService.getUser(userId: e.id).first),
);
await chatService.createChat(
isGroupChat: true,
users: [currentUser, ...otherUsers],
chatName: title,
description: description,
imageUrl: imageUrl,
);
chat = await chatService.getGroupChatByUser(
currentUser: userId,
otherUsers: otherUsers,
chatName: title,
description: description,
);
if (chat == null) {
throw Exception("Group chat not created");
}
}
return chat;
}
/// Routes to a new screen for the userstory
Future _routeToScreen(BuildContext context, Widget screen) async =>
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => screen),
);
/// Replaces the current screen with a new screen for the userstory
Future _replaceCurrentScreen(BuildContext context, Widget screen) async =>
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => screen),
);

View file

@ -1,583 +0,0 @@
import "dart:async";
import "dart:typed_data";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/flutter_chat.dart";
import "package:flutter_chat/src/screens/chat_detail/widgets/chat_bottom.dart";
import "package:flutter_chat/src/screens/chat_detail/widgets/chat_widgets.dart";
import "package:flutter_chat/src/screens/creation/widgets/default_image_picker.dart";
import "package:flutter_hooks/flutter_hooks.dart";
/// Chat detail screen
/// Seen when a user clicks on a chat
class ChatDetailScreen extends HookWidget {
/// Constructs a [ChatDetailScreen].
const ChatDetailScreen({
required this.chatId,
required this.onExit,
required this.onPressChatTitle,
required this.onPressUserProfile,
required this.onUploadImage,
required this.onMessageSubmit,
required this.onReadChat,
super.key,
});
/// The identifier of the chat that is being viewed.
/// The chat will be fetched from the chat service.
final String chatId;
/// Callback function triggered when the chat title is pressed.
final Function(ChatModel) onPressChatTitle;
/// Callback function triggered when the user profile is pressed.
final Function(UserModel) onPressUserProfile;
/// Callback function triggered when an image is uploaded.
final Function(Uint8List image) onUploadImage;
/// Callback function triggered when a message is submitted.
final Function(String text) onMessageSubmit;
/// Callback function triggered when the chat is read.
final Function(ChatModel chat) onReadChat;
/// Callback for when the user wants to navigate back
final VoidCallback? onExit;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var service = chatScope.service;
var chatTitle = useState<String?>(null);
var chatStream = useMemoized(
() => service.getChat(chatId: chatId),
[chatId],
);
var chatSnapshot = useStream(chatStream);
var chat = chatSnapshot.data;
var allUsersStream = useMemoized(
() => service.getAllUsersForChat(chatId: chatId),
[chatId],
);
var usersSnapshot = useStream(allUsersStream);
var allUsers = usersSnapshot.data ?? [];
var chatIsloading =
chatSnapshot.connectionState == ConnectionState.waiting ||
usersSnapshot.connectionState == ConnectionState.waiting;
useEffect(
() {
if (chat == null) return;
chatTitle.value = _getChatTitle(
chatScope: chatScope,
chat: chat,
allUsers: allUsers,
);
return;
},
[chat, allUsers],
);
useEffect(
() {
if (onExit == null) return null;
chatScope.popHandler.add(onExit!);
return () => chatScope.popHandler.remove(onExit!);
},
[onExit],
);
var appBar = _ChatAppBar(
chatTitle: chatTitle.value,
onPressChatTitle: onPressChatTitle,
chatModel: chat,
onPressBack: onExit,
);
var body = _ChatBody(
chatId: chatId,
chat: chat,
chatUsers: allUsers,
onPressUserProfile: onPressUserProfile,
onUploadImage: onUploadImage,
onMessageSubmit: onMessageSubmit,
onReadChat: onReadChat,
chatIsLoading: chatIsloading,
);
if (options.builders.chatScreenBuilder != null) {
return options.builders.chatScreenBuilder!.call(
context,
chat,
appBar,
chatTitle.value,
body,
);
}
if (options.builders.baseScreenBuilder != null) {
return options.builders.baseScreenBuilder!.call(
context,
mapScreenType,
appBar,
chatTitle.value,
body,
);
}
return Scaffold(
appBar: appBar,
body: body,
);
}
String? _getChatTitle({
required ChatScope chatScope,
required ChatModel chat,
required List<UserModel> allUsers,
}) {
var options = chatScope.options;
var translations = options.translations;
var title = options.chatTitleResolver?.call(chat);
if (title != null) {
return title;
}
if (chat.isGroupChat) {
if (chat.chatName?.isNotEmpty ?? false) {
return chat.chatName;
}
return translations.groupNameEmpty;
}
// For one-to-one, pick the 'other' user from the list
var otherUser = allUsers
.where(
(u) => u.id != chatScope.userId,
)
.firstOrNull;
return otherUser != null && otherUser.fullname != null
? otherUser.fullname
: translations.anonymousUser;
}
}
/// The app bar widget for the chat detail screen
class _ChatAppBar extends StatelessWidget implements PreferredSizeWidget {
const _ChatAppBar({
required this.chatTitle,
required this.chatModel,
required this.onPressChatTitle,
this.onPressBack,
});
final String? chatTitle;
final ChatModel? chatModel;
final Function(ChatModel) onPressChatTitle;
final VoidCallback? onPressBack;
@override
Widget build(BuildContext context) {
var options = ChatScope.of(context).options;
var theme = Theme.of(context);
VoidCallback? onPressChatTitle;
if (chatModel != null) {
onPressChatTitle = () => this.onPressChatTitle(chatModel!);
}
Widget? appBarIcon;
if (onPressBack != null) {
appBarIcon = CustomSemantics(
identifier: options.semantics.chatBackButton,
child: InkWell(
onTap: onPressBack,
child: const Icon(Icons.arrow_back_ios),
),
);
}
return AppBar(
iconTheme: theme.appBarTheme.iconTheme,
centerTitle: true,
leading: appBarIcon,
title: CustomSemantics(
identifier: options.semantics.chatTitleButton,
buttonWithVariableText: true,
child: InkWell(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
onTap: onPressChatTitle,
child: CustomSemantics(
identifier: options.semantics.chatChatTitle,
value: chatTitle ?? "",
child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ??
Text(
chatTitle ?? "",
overflow: TextOverflow.ellipsis,
),
),
),
),
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
/// Body for the chat detail screen
/// Displays messages, a scrollable list, and a bottom input field.
class _ChatBody extends HookWidget {
const _ChatBody({
required this.chatId,
required this.chat,
required this.chatUsers,
required this.onPressUserProfile,
required this.onUploadImage,
required this.onMessageSubmit,
required this.onReadChat,
required this.chatIsLoading,
});
final String chatId;
final ChatModel? chat;
final List<UserModel> chatUsers;
final Function(UserModel) onPressUserProfile;
final Function(Uint8List image) onUploadImage;
final Function(String text) onMessageSubmit;
final Function(ChatModel chat) onReadChat;
final bool chatIsLoading;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var service = chatScope.service;
var options = chatScope.options;
var isLoadingOlder = useState(false);
var isLoadingNewer = useState(false);
var hasMoreOlder = useState(true);
var autoScrollEnabled = useState(true);
var messagesStream = useMemoized(
() => service.getMessages(chatId: chatId),
[chatId],
);
var messagesSnapshot = useStream(messagesStream);
var messages = messagesSnapshot.data ?? [];
var scrollController = useScrollController();
Future<void> loadOlderMessages() async {
if (!hasMoreOlder.value || messages.isEmpty || isLoadingOlder.value) {
return;
}
isLoadingOlder.value = true;
var oldestMsg = messages.first;
var oldOffset = scrollController.offset;
var oldMaxScroll = scrollController.position.maxScrollExtent;
var oldCount = messages.length;
try {
await Future.wait([
service.loadOldMessagesBefore(firstMessage: oldestMsg),
Future.delayed(
options.paginationControls.loadingOldMessageMinDuration,
),
]);
} finally {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!context.mounted) return;
if (!scrollController.hasClients) {
isLoadingOlder.value = false;
return;
}
var newCount = messages.length;
if (newCount == oldCount) {
hasMoreOlder.value = false;
} else {
var newMaxScroll = scrollController.position.maxScrollExtent;
var diff = newMaxScroll - oldMaxScroll;
scrollController.jumpTo(oldOffset + diff);
}
isLoadingOlder.value = false;
});
}
}
Future<void> loadNewerMessages() async {
if (messages.isEmpty || isLoadingNewer.value) return;
isLoadingNewer.value = true;
var newestMsg = messages.last;
try {
await Future.wait([
service.loadNewMessagesAfter(lastMessage: newestMsg),
Future.delayed(
options.paginationControls.loadingNewMessageMinDuration,
),
]);
} finally {
if (context.mounted) {
isLoadingNewer.value = false;
}
}
}
useEffect(() {
void onScroll() {
if (!scrollController.hasClients) return;
var offset = scrollController.offset;
var maxScroll = scrollController.position.maxScrollExtent;
var threshold = options.paginationControls.scrollOffset;
var autoScrollThreshold =
options.paginationControls.autoScrollTriggerOffset;
var distanceFromBottom = maxScroll - offset;
if (options.paginationControls.loadOldMessagesOnScroll) {
if (offset <= threshold && !isLoadingOlder.value) {
unawaited(loadOlderMessages());
}
}
if (options.paginationControls.loadNewMessagesOnScroll) {
if (distanceFromBottom <= threshold &&
!isLoadingNewer.value &&
!autoScrollEnabled.value) {
unawaited(loadNewerMessages());
}
}
if (distanceFromBottom > autoScrollThreshold) {
autoScrollEnabled.value = false;
} else {
autoScrollEnabled.value = true;
}
}
scrollController.addListener(onScroll);
return () => scrollController.removeListener(onScroll);
}, [
scrollController,
isLoadingOlder.value,
isLoadingNewer.value,
chat,
]);
useEffect(
() {
var disposed = false;
/// Continuously scroll to the bottom of the chat
Future<void> scrollLoop() async {
while (!disposed && autoScrollEnabled.value) {
await Future.delayed(const Duration(milliseconds: 500));
if (disposed || !autoScrollEnabled.value) break;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (disposed || !autoScrollEnabled.value) return;
if (scrollController.hasClients) {
scrollController.jumpTo(
scrollController.position.maxScrollExtent,
);
}
});
}
}
unawaited(scrollLoop());
return () => disposed = true;
},
[autoScrollEnabled.value],
);
var chatBottomInputSection = ChatBottomInputSection(
chat: chat,
isLoading: chatIsLoading && !messagesSnapshot.hasData,
onPressSelectImage: () async => onPressSelectImage(
context,
options,
onUploadImage,
),
onMessageSubmit: onMessageSubmit,
);
if (messagesSnapshot.hasError) {
var errorBuilder = options.builders.chatMessagesErrorBuilder;
if (errorBuilder != null) {
return Column(
children: [
Expanded(
child: errorBuilder(
context,
messagesSnapshot.error!,
messagesSnapshot.stackTrace!,
options,
),
),
chatBottomInputSection,
],
);
}
return ErrorLoadingMessages(
options: options,
chatBottomInputSection: chatBottomInputSection,
);
}
var userMap = <String, UserModel>{};
for (var u in chatUsers) {
userMap[u.id] = u;
}
var topSpinner = (isLoadingOlder.value &&
options.paginationControls.loadingIndicatorForOldMessages)
? options.builders.loadingChatMessageBuilder.call(context)
: const SizedBox.shrink();
var bottomSpinner = (isLoadingNewer.value &&
options.paginationControls.loadingIndicatorForNewMessages)
? options.builders.loadingChatMessageBuilder.call(context)
: const SizedBox.shrink();
var bubbleChildren = <Widget>[];
if (messages.isEmpty) {
bubbleChildren
.add(ChatNoMessages(isGroupChat: chat?.isGroupChat ?? false));
} else {
for (var (index, currentMessage) in messages.indexed) {
var previousMessage = index > 0 ? messages[index - 1] : null;
if (options.timeIndicatorOptions.isMessageInNewTimeSection(
context,
previousMessage,
currentMessage,
)) {
bubbleChildren.add(
ChatTimeIndicator(
forDate: currentMessage.timestamp,
),
);
}
bubbleChildren.add(
ChatBubble(
message: currentMessage,
previousMessage: previousMessage,
sender: userMap[currentMessage.senderId],
onPressSender: onPressUserProfile,
semanticIdTitle: options.semantics.chatBubbleTitle(index),
semanticIdTime: options.semantics.chatBubbleTime(index),
semanticIdText: options.semantics.chatBubbleText(index),
),
);
}
}
var listViewChildren = [
topSpinner,
...bubbleChildren,
bottomSpinner,
];
var messageList = ListView.builder(
reverse: false,
controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.only(top: 24),
itemCount: listViewChildren.length,
itemBuilder: (context, index) => listViewChildren[index],
);
return Column(
children: [
if (chatIsLoading && options.enableLoadingIndicator) ...[
Expanded(
child: _CloseKeyboardOnTap(
child: options.builders.loadingWidgetBuilder.call(context),
),
),
] else ...[
Expanded(
child: _CloseKeyboardOnTap(
child: messageList,
),
),
],
chatBottomInputSection,
],
);
}
}
class _CloseKeyboardOnTap extends StatelessWidget {
const _CloseKeyboardOnTap({
required this.child,
});
final Widget child;
@override
Widget build(BuildContext context) => GestureDetector(
behavior: HitTestBehavior.translucent,
onTapUp: (_) {
var mediaQuery = MediaQuery.of(context);
if (mediaQuery.viewInsets.isNonNegative) {
FocusScope.of(context).unfocus();
}
},
child: child,
);
}
/// Default widget used when displaying an error for chats.
class ErrorLoadingMessages extends StatelessWidget {
/// Create default error displaying widget for error in loading messages
const ErrorLoadingMessages({
required this.options,
required this.chatBottomInputSection,
super.key,
});
/// the options of the current chat userstory
final ChatOptions options;
/// The widget
final ChatBottomInputSection chatBottomInputSection;
@override
Widget build(BuildContext context) => Column(
children: [
Expanded(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
options.translations.messagesLoadingError,
),
],
),
),
),
chatBottomInputSection,
],
);
}

View file

@ -1,180 +0,0 @@
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_hooks/flutter_hooks.dart";
/// Chat Bottom section where the user can type or upload images.
class ChatBottomInputSection extends HookWidget {
/// Creates a new [ChatBottomInputSection].
const ChatBottomInputSection({
required this.chat,
required this.isLoading,
required this.onMessageSubmit,
this.onPressSelectImage,
super.key,
});
/// The chat model.
final ChatModel? chat;
/// Whether the chat is still loading.
/// The inputfield is disabled when the chat is loading.
final bool isLoading;
/// Callback function invoked when a message is submitted.
final Function(String text) onMessageSubmit;
/// Callback function invoked when the select image button is pressed.
final VoidCallback? onPressSelectImage;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var theme = Theme.of(context);
var textController = useTextEditingController();
var isTyping = useState(false);
var isSending = useState(false);
useEffect(
() {
void listener() => isTyping.value = textController.text.isNotEmpty;
textController.addListener(listener);
return () => textController.removeListener(listener);
},
[textController],
);
Future<void> sendMessage() async {
isSending.value = true;
var value = textController.text;
if (value.isNotEmpty) {
await onMessageSubmit(value);
textController.clear();
}
isSending.value = false;
}
Future<void> Function()? onClickSendMessage;
if (isTyping.value && !isSending.value) {
onClickSendMessage = () async => sendMessage();
}
/// Image and send buttons
var messageSendButtons = Padding(
padding: const EdgeInsets.only(right: 6.0),
child: SizedBox(
height: 48,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
CustomSemantics(
identifier: options.semantics.chatSelectImageIconButton,
child: IconButton(
alignment: Alignment.bottomRight,
onPressed: isLoading ? null : onPressSelectImage,
icon: Icon(
Icons.image_outlined,
color: options.iconEnabledColor,
),
),
),
CustomSemantics(
identifier: options.semantics.chatSendMessageIconButton,
child: IconButton(
alignment: Alignment.bottomRight,
disabledColor: options.iconDisabledColor,
color: options.iconEnabledColor,
onPressed: isLoading ? null : onClickSendMessage,
icon: const Icon(Icons.send_rounded),
),
),
],
),
),
);
Future<void> onSubmitField() async => sendMessage();
var defaultInputField = Stack(
children: [
CustomSemantics(
identifier: options.semantics.chatMessageInput,
isTextField: true,
child: TextField(
textAlign: TextAlign.start,
textAlignVertical: TextAlignVertical.center,
style: theme.textTheme.bodySmall,
textCapitalization: TextCapitalization.sentences,
textInputAction: TextInputAction.newline,
keyboardType: TextInputType.multiline,
maxLines: null,
controller: textController,
enabled: !isLoading,
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
borderSide: const BorderSide(color: Colors.black),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
borderSide: const BorderSide(color: Colors.black),
),
contentPadding: const EdgeInsets.only(
left: 16,
top: 16,
bottom: 16,
),
// this ensures that that there is space at the end of the
// textfield
suffixIcon: ExcludeFocus(
child: AbsorbPointer(
child: Opacity(
opacity: 0.0,
child: messageSendButtons,
),
),
),
hintText: options.translations.messagePlaceholder,
hintStyle: theme.textTheme.bodyMedium,
fillColor: Colors.white,
filled: true,
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(25)),
borderSide: BorderSide.none,
),
),
onSubmitted: (_) async => onSubmitField(),
),
),
Positioned(
right: 0,
bottom: 0,
child: messageSendButtons,
),
],
);
return Padding(
padding: EdgeInsets.symmetric(
horizontal: options.spacing.chatSidePadding,
vertical: 16,
),
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 120, minHeight: 45),
child: options.builders.messageInputBuilder?.call(
context,
textEditingController: textController,
suffixIcon: messageSendButtons,
translations: options.translations,
onSubmit: onSubmitField,
enabled: !isLoading,
) ??
defaultInputField,
),
);
}
}

View file

@ -1,134 +0,0 @@
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/screens/chat_detail/widgets/default_message_builder.dart";
import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_chat/src/util/utils.dart";
import "package:flutter_hooks/flutter_hooks.dart";
/// Widget displayed when there are no messages in the chat.
class ChatNoMessages extends HookWidget {
/// Creates a new [ChatNoMessages] widget.
const ChatNoMessages({
required this.isGroupChat,
super.key,
});
/// Determines if this chat is a group chat.
final bool isGroupChat;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var translations = options.translations;
var theme = Theme.of(context);
return Center(
child: CustomSemantics(
identifier: options.semantics.chatNoMessages,
value: isGroupChat
? translations.writeFirstMessageInGroupChat
: translations.writeMessageToStartChat,
child: Text(
isGroupChat
? translations.writeFirstMessageInGroupChat
: translations.writeMessageToStartChat,
style: theme.textTheme.bodySmall,
),
),
);
}
}
/// A single chat bubble in the chat
class ChatBubble extends HookWidget {
/// Creates a new [ChatBubble] widget.
const ChatBubble({
required this.message,
required this.sender,
required this.onPressSender,
required this.semanticIdTitle,
required this.semanticIdText,
required this.semanticIdTime,
this.previousMessage,
super.key,
});
/// The message to display.
final MessageModel message;
/// The user who sent the message. This can be null because some messages are
/// not from users
final UserModel? sender;
/// The previous message in the list, if any.
final MessageModel? previousMessage;
/// Callback function when a message sender is pressed.
final Function(UserModel user) onPressSender;
/// Semantic id for message title
final String semanticIdTitle;
/// Semantic id for message time
final String semanticIdTime;
/// Semantic id for message text
final String semanticIdText;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
return options.builders.chatMessageBuilder.call(
context,
message,
previousMessage,
sender,
onPressSender,
semanticIdTitle,
semanticIdTime,
semanticIdText,
) ??
DefaultChatMessageBuilder(
message: message,
previousMessage: previousMessage,
sender: sender,
onPressSender: onPressSender,
semanticIdTitle: semanticIdTitle,
semanticIdTime: semanticIdTime,
semanticIdText: semanticIdText,
);
}
}
/// The indicator above a set of messages, shown per date.
class ChatTimeIndicator extends StatelessWidget {
/// Creates a ChatTimeIndicator
const ChatTimeIndicator({
required this.forDate,
super.key,
});
/// The dateTime at which the new time section starts
final DateTime forDate;
@override
Widget build(BuildContext context) {
var scope = ChatScope.of(context);
var indicatorOptions = scope.options.timeIndicatorOptions;
var today = DateTime.now();
var differenceInDays = today.getDateOffsetInDays(forDate);
var message = indicatorOptions.labelResolver(
context,
differenceInDays,
forDate,
);
return indicatorOptions.indicatorBuilder(context, message);
}
}

View file

@ -1,43 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_chat/flutter_chat.dart";
/// The default layout for a chat indicator
class DefaultChatTimeIndicator extends StatelessWidget {
/// Create a default timeindicator in a chat
const DefaultChatTimeIndicator({
required this.timeIndicatorString,
super.key,
});
/// The text shown in the time indicator
final String timeIndicatorString;
/// Standard builder for time indication
static Widget builder(BuildContext context, String timeIndicatorString) =>
DefaultChatTimeIndicator(timeIndicatorString: timeIndicatorString);
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var spacing = ChatScope.of(context).options.spacing;
return Center(
child: Container(
margin: EdgeInsets.only(top: spacing.chatBetweenMessagesPadding),
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 8,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: theme.colorScheme.surfaceContainerHighest,
),
child: Text(
timeIndicatorString,
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
);
}
}

View file

@ -1,38 +0,0 @@
import "package:flutter/material.dart";
/// Default chat loading overlay
/// This is displayed over the chat when loading
class DefaultChatLoadingOverlay extends StatelessWidget {
/// Creates a new default chat loading overlay
const DefaultChatLoadingOverlay({super.key});
/// Builds the default chat loading overlay
static Widget builder(BuildContext context) =>
const DefaultChatLoadingOverlay();
@override
Widget build(BuildContext context) =>
const Center(child: CircularProgressIndicator());
}
/// A small row spinner item to show partial loading
class DefaultChatMessageLoader extends StatelessWidget {
/// Creates a new default chat message loader
const DefaultChatMessageLoader({super.key});
/// Builds the default chat message loader
static Widget builder(BuildContext context) =>
const DefaultChatMessageLoader();
@override
Widget build(BuildContext context) => const Padding(
padding: EdgeInsets.all(8.0),
child: Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
);
}

View file

@ -1,504 +0,0 @@
import "dart:async";
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/config/chat_options.dart";
import "package:flutter_chat/src/services/date_formatter.dart";
import "package:flutter_chat/src/util/scope.dart";
/// The default chat message builder that shows messages aligned to the left or
/// right depending on the sender.
/// It can be styled using the [MessageTheme] from the [ChatOptions].
class DefaultChatMessageBuilder extends StatelessWidget {
/// Creates a new [DefaultChatMessageBuilder]
const DefaultChatMessageBuilder({
required this.message,
required this.previousMessage,
required this.sender,
required this.onPressSender,
required this.semanticIdTitle,
required this.semanticIdText,
required this.semanticIdTime,
super.key,
});
/// The message that is being built
final MessageModel message;
/// The previous message if any, this can be used to determine if the message
/// is from the same sender as the previous message.
final MessageModel? previousMessage;
/// The user that sent the message, can be null if the message is an event
final UserModel? sender;
/// The function that is called when the sender is clicked
final Function(UserModel user) onPressSender;
/// Semantic id for message title
final String semanticIdTitle;
/// Semantic id for message time
final String semanticIdTime;
/// Semantic id for message text
final String semanticIdText;
/// implements [ChatMessageBuilder]
static Widget builder(
BuildContext context,
MessageModel message,
MessageModel? previousMessage,
UserModel? sender,
Function(UserModel sender) onPressSender,
String semanticIdTitle,
String semanticIdText,
String semanticIdTime,
) =>
DefaultChatMessageBuilder(
message: message,
previousMessage: previousMessage,
sender: sender,
onPressSender: onPressSender,
semanticIdTitle: semanticIdTitle,
semanticIdTime: semanticIdTime,
semanticIdText: semanticIdText,
);
/// Merges the [MessageTheme] from the themeresolver with the [MessageTheme]
/// from the options and the [MessageTheme] from the theme. Priority is given
/// to the [MessageTheme] from the themeresolver.
MessageTheme _resolveMessageTheme({
required BuildContext context,
required ChatOptions options,
required MessageModel message,
required MessageModel? previousMessage,
required UserModel? user,
}) =>
[
options.messageThemeResolver(context, message, previousMessage, user),
options.messageTheme,
MessageTheme.fromTheme(Theme.of(context)),
].whereType<MessageTheme>().reduce((value, element) => value | element);
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var userId = chatScope.userId;
var messageTheme = _resolveMessageTheme(
context: context,
options: options,
message: message,
previousMessage: previousMessage,
user: sender,
);
var isSameSender = previousMessage != null &&
previousMessage?.senderId == message.senderId;
var hasPreviousIndicator = options.timeIndicatorOptions.sectionCheck(
context,
previousMessage,
message,
);
var isMessageFromSelf = message.senderId == userId;
var chatMessage = _ChatMessageBubble(
isSameSender: isSameSender,
hasPreviousIndicator: hasPreviousIndicator,
isMessageFromSelf: isMessageFromSelf,
previousMessage: previousMessage,
message: message,
messageTheme: messageTheme,
sender: sender,
semanticIdTitle: semanticIdTitle,
semanticIdTime: semanticIdTime,
semanticIdText: semanticIdText,
);
var messagePadding = messageTheme.messageSidePadding!;
var standardAlignmentIfNull =
isMessageFromSelf ? TextAlign.right : TextAlign.left;
var leftPaddingMessage =
switch (messageTheme.messageAlignment ?? standardAlignmentIfNull) {
TextAlign.left => 0.0,
TextAlign.right => messagePadding,
_ => messagePadding / 2,
};
var rightPadding =
switch (messageTheme.messageAlignment ?? standardAlignmentIfNull) {
TextAlign.left => messagePadding,
TextAlign.right => 0.0,
_ => messagePadding / 2,
};
return Row(
children: [
SizedBox(width: leftPaddingMessage + options.spacing.chatSidePadding),
chatMessage,
SizedBox(width: rightPadding + options.spacing.chatSidePadding),
],
);
}
}
class _ChatMessageStatus extends StatelessWidget {
const _ChatMessageStatus({
required this.messageTheme,
required this.status,
});
final MessageTheme messageTheme;
final MessageStatus status;
@override
Widget build(BuildContext context) => switch (status) {
MessageStatus.sending => Icon(
Icons.access_time,
size: 16.0,
color: messageTheme.textColor,
),
MessageStatus.sent => Icon(
Icons.check,
size: 16.0,
color: messageTheme.textColor,
),
};
}
class _ChatMessageBubble extends StatelessWidget {
const _ChatMessageBubble({
required this.isSameSender,
required this.hasPreviousIndicator,
required this.isMessageFromSelf,
required this.message,
required this.previousMessage,
required this.messageTheme,
required this.sender,
required this.semanticIdTitle,
required this.semanticIdTime,
required this.semanticIdText,
});
final bool isSameSender;
final bool hasPreviousIndicator;
final bool isMessageFromSelf;
final MessageModel message;
final MessageModel? previousMessage;
final MessageTheme messageTheme;
final UserModel? sender;
final String semanticIdTitle;
final String semanticIdTime;
final String semanticIdText;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var textTheme = theme.textTheme;
var options = ChatScope.of(context).options;
var dateFormatter = DateFormatter(options: options);
var isNewDate = previousMessage != null &&
message.timestamp.day != previousMessage?.timestamp.day;
var showFullDateOnMessage =
messageTheme.showFullDate ?? (isNewDate || previousMessage == null);
var messageTime = dateFormatter.format(
date: message.timestamp.toLocal(),
showFullDate: showFullDateOnMessage,
);
var senderTitle =
options.senderTitleResolver?.call(sender) ?? sender?.firstName ?? "";
var senderTitleText = CustomSemantics(
identifier: semanticIdTitle,
value: senderTitle,
child: Text(
senderTitle,
style: theme.textTheme.titleMedium,
),
);
var messageTimeRow = Padding(
padding: const EdgeInsets.only(
bottom: 8.0,
right: 8.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
CustomSemantics(
identifier: semanticIdTime,
value: messageTime,
child: Text(
messageTime,
style: textTheme.bodySmall?.copyWith(
color: messageTheme.textColor,
),
textAlign: TextAlign.end,
),
),
const SizedBox(width: 4.0),
_ChatMessageStatus(
messageTheme: messageTheme,
status: message.status,
),
],
),
);
var showName =
messageTheme.showName ?? (!isSameSender || hasPreviousIndicator);
var isNewSection = hasPreviousIndicator || showName;
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isNewSection) ...[
SizedBox(height: options.spacing.chatBetweenMessagesPadding),
],
if (showName) senderTitleText,
const SizedBox(height: 4),
DefaultChatMessageContainer(
backgroundColor: messageTheme.backgroundColor!,
borderColor: messageTheme.borderColor!,
borderRadius: messageTheme.borderRadius!,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 4),
if (message.imageUrl?.isNotEmpty ?? false) ...[
_DefaultChatImage(
message: message,
messageTheme: messageTheme,
options: options,
),
const SizedBox(height: 2),
],
if (message.text?.isNotEmpty ?? false) ...[
Padding(
padding: const EdgeInsets.only(
top: 8,
left: 12,
right: 12,
bottom: 4,
),
child: Semantics(
identifier: semanticIdText,
value: message.text,
child: Text(
message.text!,
style: textTheme.bodyLarge?.copyWith(
color: messageTheme.textColor,
),
textAlign: messageTheme.textAlignment,
),
),
),
],
if (messageTheme.showTime!) ...[
messageTimeRow,
],
],
),
),
],
),
);
}
}
class _DefaultChatImage extends StatefulWidget {
const _DefaultChatImage({
required this.message,
required this.messageTheme,
required this.options,
});
final MessageModel message;
final ChatOptions options;
final MessageTheme messageTheme;
@override
State<_DefaultChatImage> createState() => _DefaultChatImageState();
}
/// Exception thrown when the image builder fails to recognize the image
class InvalidImageUrlException implements Exception {}
class _DefaultChatImageState extends State<_DefaultChatImage>
with AutomaticKeepAliveClientMixin {
late ImageProvider provider;
late Completer imageLoadingCompleter;
void _preloadImage() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
var uri = Uri.tryParse(widget.message.imageUrl ?? "");
if (uri == null) {
imageLoadingCompleter.completeError(InvalidImageUrlException());
return;
}
provider = widget.options.imageProviderResolver(
context,
uri,
);
if (!mounted) return;
await precacheImage(
provider,
context,
onError: imageLoadingCompleter.completeError,
);
imageLoadingCompleter.complete();
});
}
void _refreshImage() {
setState(() {
imageLoadingCompleter = Completer();
});
_preloadImage();
}
@override
void initState() {
super.initState();
imageLoadingCompleter = Completer();
_preloadImage();
}
@override
void didUpdateWidget(covariant _DefaultChatImage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.message.imageUrl != widget.message.imageUrl) {
_refreshImage();
}
}
@override
Widget build(BuildContext context) {
super.build(context);
var theme = Theme.of(context);
var asyncImageBuilder = FutureBuilder<void>(
future: imageLoadingCompleter.future,
builder: (context, snapshot) => switch (snapshot.connectionState) {
ConnectionState.waiting => Center(
child: CircularProgressIndicator(
color: widget.messageTheme.textColor,
),
),
ConnectionState.done when !snapshot.hasError => Image(
image: provider,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
_DefaultMessageImageError(
messageTheme: widget.messageTheme,
onRefresh: _refreshImage,
),
),
_ => _DefaultMessageImageError(
messageTheme: widget.messageTheme,
onRefresh: _refreshImage,
),
},
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: SizedBox(
width: double.infinity,
child: LayoutBuilder(
builder: (context, constraints) => ConstrainedBox(
constraints: BoxConstraints.tightForFinite(
width: constraints.maxWidth,
height: constraints.maxWidth,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: ColoredBox(
color: widget.messageTheme.imageBackgroundColor ??
theme.colorScheme.secondaryContainer,
child: asyncImageBuilder,
),
),
),
),
),
);
}
@override
bool get wantKeepAlive => true;
}
class _DefaultMessageImageError extends StatelessWidget {
const _DefaultMessageImageError({
required this.messageTheme,
required this.onRefresh,
});
final MessageTheme messageTheme;
final VoidCallback onRefresh;
@override
Widget build(BuildContext context) => Center(
child: IconButton(
onPressed: onRefresh,
icon: Icon(
Icons.refresh,
color: messageTheme.textColor,
),
),
);
}
/// A container for the chat message that provides a decoration around the
/// message
class DefaultChatMessageContainer extends StatelessWidget {
/// Creates a new [DefaultChatMessageContainer]
const DefaultChatMessageContainer({
required this.backgroundColor,
required this.borderColor,
required this.borderRadius,
required this.child,
super.key,
});
/// The color of the message background
final Color backgroundColor;
/// The color of the border around the message
final Color borderColor;
/// The border radius of the message container
final BorderRadius borderRadius;
/// The content of the message
final Widget child;
@override
Widget build(BuildContext context) => DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: borderRadius,
border: Border.all(
width: 1,
color: borderColor,
),
),
child: child,
);
}

View file

@ -1,207 +0,0 @@
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_chat/src/services/date_formatter.dart";
import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_profile/flutter_profile.dart";
/// The old chat message builder that shows messages with the user image on the
/// left and the message on the right.
class OldChatMessageBuilder extends StatelessWidget {
/// Creates a new [OldChatMessageBuilder]
const OldChatMessageBuilder({
required this.message,
required this.previousMessage,
required this.user,
required this.onPressUserProfile,
super.key,
});
/// The message that is being built
final MessageModel message;
/// The previous message if any, this can be used to determine if the message
/// is from the same sender as the previous message.
final MessageModel? previousMessage;
/// The user that sent the message
final UserModel user;
/// The function that is called when the user profile is pressed
final Function(UserModel user) onPressUserProfile;
/// implements [ChatMessageBuilder]
static Widget builder(
BuildContext context,
MessageModel message,
MessageModel? previousMessage,
UserModel user,
Function(UserModel user) onPressUserProfile,
) =>
OldChatMessageBuilder(
message: message,
previousMessage: previousMessage,
user: user,
onPressUserProfile: onPressUserProfile,
);
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var translations = options.translations;
var theme = Theme.of(context);
var dateFormatter = DateFormatter(options: options);
var isNewDate = previousMessage != null &&
message.timestamp.day != previousMessage?.timestamp.day;
var isSameSender = previousMessage == null ||
previousMessage?.senderId != message.senderId;
var isSameMinute = previousMessage != null &&
message.timestamp.minute == previousMessage?.timestamp.minute;
var hasHeader = isNewDate || isSameSender;
return Padding(
padding: EdgeInsets.only(
top: hasHeader ? 25.0 : 0,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (hasHeader) ...[
InkWell(
onTap: () => onPressUserProfile(user),
child: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: user.imageUrl?.isNotEmpty ?? false
? _ChatImage(
image: user.imageUrl!,
)
: options.builders.userAvatarBuilder?.call(
context,
user,
40,
) ??
Avatar(
key: ValueKey(user.id),
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl:
user.imageUrl != "" ? user.imageUrl : null,
),
size: 40,
),
),
),
] else ...[
const SizedBox(width: 50),
],
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (hasHeader) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: options.builders.usernameBuilder?.call(
user.fullname ?? "",
) ??
Text(
user.fullname ?? translations.anonymousUser,
style: theme.textTheme.titleMedium,
),
),
Padding(
padding: const EdgeInsets.only(top: 5.0),
child: Text(
dateFormatter.format(
date: message.timestamp,
showFullDate: true,
),
style: theme.textTheme.labelSmall,
),
),
],
),
],
Padding(
padding: const EdgeInsets.only(top: 3.0),
child: message.isTextMessage
? Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
message.text ?? "",
style: theme.textTheme.bodySmall,
),
),
if (!isSameMinute && !isNewDate && !hasHeader)
Text(
dateFormatter
.format(
date: message.timestamp,
showFullDate: true,
)
.split(" ")
.last,
style: theme.textTheme.labelSmall,
textAlign: TextAlign.end,
),
],
)
: message.isImageMessage
? Image(
image: options.imageProviderResolver(
context,
Uri.parse(message.imageUrl!),
),
)
: const SizedBox.shrink(),
),
],
),
),
),
],
),
);
}
}
class _ChatImage extends StatelessWidget {
const _ChatImage({
required this.image,
});
final String image;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
return Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(40.0),
),
width: 40,
height: 40,
child: image.isNotEmpty
? Image(
fit: BoxFit.cover,
image: options.imageProviderResolver(context, Uri.parse(image)),
)
: null,
);
}
}

View file

@ -1,320 +0,0 @@
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/config/screen_types.dart";
import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:flutter_profile/flutter_profile.dart";
/// The chat profile screen
/// Seen when a user taps on a chat profile
/// Also used for group chats
class ChatProfileScreen extends HookWidget {
/// Constructs a [ChatProfileScreen]
const ChatProfileScreen({
required this.onExit,
required this.userModel,
required this.chatModel,
required this.onTapUser,
required this.onPressStartChat,
super.key,
});
/// The user model of the persons profile to be viewed
final UserModel? userModel;
/// The chat model of the chat being viewed
final ChatModel? chatModel;
/// Callback function triggered when a user is tapped
final Function(String)? onTapUser;
/// Callback function triggered when the start chat button is pressed
final Function(String)? onPressStartChat;
/// Callback for when the user wants to navigate back
final VoidCallback onExit;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
useEffect(() {
chatScope.popHandler.add(onExit);
return () => chatScope.popHandler.remove(onExit);
});
var chatTitle = userModel != null
? "${userModel!.fullname}"
: chatModel != null
? chatModel?.chatName ?? options.translations.groupNameEmpty
: "";
var appBar = _AppBar(
title: chatTitle,
semanticId: options.semantics.profileTitle,
);
var body = _Body(
user: userModel,
chat: chatModel,
onTapUser: onTapUser,
onPressStartChat: onPressStartChat,
);
if (options.builders.baseScreenBuilder == null) {
return Scaffold(
appBar: appBar,
body: body,
);
}
return options.builders.baseScreenBuilder!.call(
context,
mapScreenType,
appBar,
chatTitle,
body,
);
}
}
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
const _AppBar({
required this.title,
required this.semanticId,
});
final String title;
final String semanticId;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return AppBar(
iconTheme: theme.appBarTheme.iconTheme,
title: CustomSemantics(
identifier: semanticId,
value: title,
child: Text(title),
),
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _Body extends StatelessWidget {
const _Body({
required this.user,
required this.chat,
required this.onPressStartChat,
required this.onTapUser,
});
final UserModel? user;
final ChatModel? chat;
final Function(String)? onTapUser;
final Function(String)? onPressStartChat;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var service = chatScope.service;
var currentUser = chatScope.userId;
var theme = Theme.of(context);
var chatUserDisplay = Wrap(
children: [
if (chat != null) ...[
...chat!.users.asMap().entries.map(
(entry) {
var index = entry.key;
var tappedUser = entry.value;
return Padding(
padding: const EdgeInsets.only(
bottom: 8,
right: 8,
),
child: CustomSemantics(
identifier: options.semantics.profileTapUserButton(index),
child: InkWell(
onTap: () => onTapUser?.call(tappedUser),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FutureBuilder<UserModel>(
future: service.getUser(userId: tappedUser).first,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const CircularProgressIndicator();
}
var user = snapshot.data;
if (user == null) {
return const SizedBox.shrink();
}
return options.builders.userAvatarBuilder?.call(
context,
user,
44,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl: user.imageUrl != null ||
user.imageUrl != ""
? user.imageUrl
: null,
),
size: 60,
);
},
),
],
),
),
),
);
},
),
],
],
);
var targetUser = user ??
(
chat != null
? UserModel(
id: UniqueKey().toString(),
firstName: chat?.chatName,
imageUrl: chat?.imageUrl,
)
: UserModel(
id: UniqueKey().toString(),
firstName: options.translations.groupNameEmpty,
),
) as UserModel;
return Stack(
children: [
ListView(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
children: [
options.builders.userAvatarBuilder?.call(
context,
targetUser,
60,
) ??
Avatar(
boxfit: BoxFit.cover,
user: user != null
? User(
firstName: user?.firstName,
lastName: user?.lastName,
imageUrl: user?.imageUrl != null ||
user?.imageUrl != ""
? user?.imageUrl
: null,
)
: chat != null
? User(
firstName: chat?.chatName,
imageUrl: chat?.imageUrl != null ||
chat?.imageUrl != ""
? chat?.imageUrl
: null,
)
: User(
firstName:
options.translations.groupNameEmpty,
),
size: 60,
),
],
),
),
const Divider(
color: Colors.white,
thickness: 10,
),
if (chat != null) ...[
Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 20,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
options.translations.groupProfileBioHeader,
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 12),
CustomSemantics(
identifier: options.semantics.profileDescription,
value: chat!.description ?? "",
child: Text(
chat!.description ?? "",
style: theme.textTheme.bodyMedium,
),
),
const SizedBox(height: 12),
Text(
options.translations.chatProfileUsers,
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 12),
chatUserDisplay,
],
),
),
],
],
),
if (user?.id != currentUser) ...[
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 80,
),
child: CustomSemantics(
identifier: options.semantics.profileStartChatButton,
child: FilledButton(
onPressed: () {
onPressStartChat?.call(user!.id);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
options.translations.newChatButton,
style: theme.textTheme.displayLarge,
),
],
),
),
),
),
),
],
],
);
}
}

View file

@ -1,720 +0,0 @@
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/config/chat_translations.dart";
import "package:flutter_chat/src/config/screen_types.dart";
import "package:flutter_chat/src/services/date_formatter.dart";
import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:flutter_profile/flutter_profile.dart";
/// The chat screen
/// Seen when a user is chatting
class ChatScreen extends HookWidget {
/// Constructs a [ChatScreen]
const ChatScreen({
required this.onPressChat,
required this.onDeleteChat,
required this.onExit,
this.onPressStartChat,
super.key,
});
/// Callback function for starting a chat.
final Function()? onPressStartChat;
/// Callback function for pressing on a chat.
final void Function(ChatModel chat) onPressChat;
/// Callback function for deleting a chat.
final void Function(ChatModel chat) onDeleteChat;
/// Callback for when the user wants to navigate back
final VoidCallback? onExit;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var translations = options.translations;
useEffect(() {
if (onExit == null) return null;
chatScope.popHandler.add(onExit!);
return () => chatScope.popHandler.remove(onExit!);
});
if (options.builders.baseScreenBuilder == null) {
return Scaffold(
appBar: const _AppBar(),
body: _Body(
onPressChat: onPressChat,
onPressStartChat: onPressStartChat,
onDeleteChat: onDeleteChat,
),
);
}
return options.builders.baseScreenBuilder!.call(
context,
mapScreenType,
const _AppBar(),
translations.chatsTitle,
_Body(
onPressChat: onPressChat,
onPressStartChat: onPressStartChat,
onDeleteChat: onDeleteChat,
),
);
}
}
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
const _AppBar();
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var service = chatScope.service;
var options = chatScope.options;
var translations = options.translations;
var theme = Theme.of(context);
return AppBar(
title: Text(
translations.chatsTitle,
),
actions: [
StreamBuilder<int>(
stream: service.getUnreadMessagesCount(),
builder: (BuildContext context, snapshot) => Align(
alignment: Alignment.centerRight,
child: Visibility(
visible: (snapshot.data ?? 0) > 0,
child: Padding(
padding: const EdgeInsets.only(right: 22.0),
child: CustomSemantics(
identifier: options.semantics.chatUnreadMessages,
value: "${snapshot.data ?? 0} ${translations.chatsUnread}",
child: Text(
"${snapshot.data ?? 0} ${translations.chatsUnread}",
style: theme.textTheme.bodySmall,
),
),
),
),
),
),
],
);
}
@override
Size get preferredSize => const Size.fromHeight(
kToolbarHeight,
);
}
class _Body extends StatefulWidget {
const _Body({
required this.onPressChat,
required this.onDeleteChat,
this.onPressStartChat,
});
final Function(ChatModel chat) onPressChat;
final Function()? onPressStartChat;
final Function(ChatModel) onDeleteChat;
@override
State<_Body> createState() => _BodyState();
}
class _BodyState extends State<_Body> {
final ScrollController controller = ScrollController();
bool _hasCalledOnNoChats = false;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var service = chatScope.service;
var translations = options.translations;
var theme = Theme.of(context);
return Column(
children: [
Expanded(
child: ListView(
controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 28),
children: [
StreamBuilder<List<ChatModel>?>(
stream: service.getChats(),
builder: (BuildContext context, snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
(snapshot.data?.isEmpty ?? true) ||
(snapshot.data != null && snapshot.data!.isEmpty)) {
if (options.onNoChats != null && !_hasCalledOnNoChats) {
_hasCalledOnNoChats = true; // Set the flag to true
WidgetsBinding.instance.addPostFrameCallback((_) async {
// ignore: avoid_dynamic_calls
await options.onNoChats!.call();
});
}
return Center(
child: Text(
translations.noChatsFound,
style: theme.textTheme.bodySmall,
),
);
}
return Column(
children: [
for (var (index, ChatModel chat)
in (snapshot.data ?? []).indexed) ...[
DecoratedBox(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: theme.dividerColor,
width: 0.5,
),
),
),
child: Builder(
builder: (context) {
var semantics = options.semantics;
var chatItem = _ChatItem(
chat: chat,
onPressChat: widget.onPressChat,
semanticIdTitle:
semantics.chatsChatTitle(index),
semanticIdSubTitle:
semantics.chatsChatSubTitle(index),
semanticIdLastUsed:
semantics.chatsChatLastUsed(index),
semanticIdUnreadMessages:
semantics.chatsChatUnreadMessages(index),
semanticIdButton:
semantics.chatsOpenChatButton(index),
);
return !chat.canBeDeleted
? Dismissible(
confirmDismiss: (_) async {
await options.builders
.deleteChatDialogBuilder
?.call(context, chat) ??
_deleteDialog(
chat,
translations,
// ignore: use_build_context_synchronously
context,
);
return _deleteDialog(
chat,
translations,
// ignore: use_build_context_synchronously
context,
);
},
onDismissed: (_) {
widget.onDeleteChat(chat);
},
secondaryBackground: const ColoredBox(
color: Colors.red,
child: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
),
background: const ColoredBox(
color: Colors.red,
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
),
key: ValueKey(
chat.id,
),
child: chatItem,
)
: chatItem;
},
),
),
],
],
);
},
),
],
),
),
if (widget.onPressStartChat != null)
options.builders.newChatButtonBuilder?.call(
context,
widget.onPressStartChat!,
translations,
) ??
Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 4,
),
child: CustomSemantics(
identifier: options.semantics.chatsStartChatButton,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
fixedSize: const Size(254, 44),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(56),
),
),
onPressed: widget.onPressStartChat,
child: Text(
translations.newChatButton,
style: theme.textTheme.displayLarge,
),
),
),
),
],
);
}
}
class _ChatItem extends StatelessWidget {
const _ChatItem({
required this.chat,
required this.onPressChat,
required this.semanticIdTitle,
required this.semanticIdSubTitle,
required this.semanticIdLastUsed,
required this.semanticIdUnreadMessages,
required this.semanticIdButton,
});
final ChatModel chat;
final Function(ChatModel chat) onPressChat;
final String semanticIdTitle;
final String semanticIdSubTitle;
final String semanticIdLastUsed;
final String semanticIdUnreadMessages;
final String semanticIdButton;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var dateFormatter = DateFormatter(
options: options,
);
var theme = Theme.of(context);
var chatListItem = _ChatListItem(
chat: chat,
dateFormatter: dateFormatter,
semanticIdTitle: semanticIdTitle,
semanticIdSubTitle: semanticIdSubTitle,
semanticIdLastUsed: semanticIdLastUsed,
semanticIdUnreadMessages: semanticIdUnreadMessages,
);
return CustomSemantics(
identifier: semanticIdButton,
buttonWithVariableText: true,
child: InkWell(
onTap: () {
onPressChat(chat);
},
child: options.builders.chatRowContainerBuilder?.call(
context,
chatListItem,
) ??
DecoratedBox(
decoration: BoxDecoration(
color: Colors.transparent,
border: Border(
bottom: BorderSide(
color: theme.dividerColor,
width: 0.5,
),
),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: chatListItem,
),
),
),
);
}
}
class _ChatListItem extends StatelessWidget {
const _ChatListItem({
required this.chat,
required this.dateFormatter,
required this.semanticIdTitle,
required this.semanticIdSubTitle,
required this.semanticIdLastUsed,
required this.semanticIdUnreadMessages,
});
final ChatModel chat;
final DateFormatter dateFormatter;
final String semanticIdTitle;
final String semanticIdSubTitle;
final String semanticIdLastUsed;
final String semanticIdUnreadMessages;
@override
Widget build(BuildContext context) {
var scope = ChatScope.of(context);
var service = scope.service;
var options = scope.options;
var currentUserId = scope.userId;
var translations = options.translations;
if (chat.isGroupChat) {
return StreamBuilder<MessageModel?>(
stream: chat.lastMessage != null
? service.getMessage(
chatId: chat.id,
messageId: chat.lastMessage!,
)
: const Stream.empty(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
var data = snapshot.data;
var showUnreadMessageCount =
data != null && data.senderId != currentUserId;
return _ChatRow(
title: chat.chatName ?? translations.groupNameEmpty,
semanticIdTitle: semanticIdTitle,
semanticIdSubTitle: semanticIdSubTitle,
semanticIdLastUsed: semanticIdLastUsed,
semanticIdUnreadMessages: semanticIdUnreadMessages,
unreadMessages:
showUnreadMessageCount ? chat.unreadMessageCount : 0,
subTitle: data != null
? data.isTextMessage
? data.text
: "📷 "
"${translations.image}"
: "",
avatar: options.builders.groupAvatarBuilder?.call(
context,
chat.chatName ?? translations.groupNameEmpty,
chat.imageUrl,
40.0,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: chat.chatName,
lastName: null,
imageUrl: chat.imageUrl != null || chat.imageUrl != ""
? chat.imageUrl
: null,
),
size: 40.0,
),
lastUsed: chat.lastUsed != null
? dateFormatter.format(
date: chat.lastUsed!,
)
: null,
);
},
);
}
var otherUser = chat.users.firstWhere(
(element) => element != currentUserId,
);
return StreamBuilder<UserModel>(
stream: service.getUser(userId: otherUser),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
var otherUser = snapshot.data;
if (otherUser == null) {
return const SizedBox();
}
return StreamBuilder<MessageModel?>(
stream: chat.lastMessage != null
? service.getMessage(
chatId: chat.id,
messageId: chat.lastMessage!,
)
: const Stream.empty(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
var data = snapshot.data;
var showUnreadMessageCount =
data != null && data.senderId != currentUserId;
return _ChatRow(
unreadMessages:
showUnreadMessageCount ? chat.unreadMessageCount : 0,
semanticIdTitle: semanticIdTitle,
semanticIdSubTitle: semanticIdSubTitle,
semanticIdLastUsed: semanticIdLastUsed,
semanticIdUnreadMessages: semanticIdUnreadMessages,
avatar: options.builders.userAvatarBuilder?.call(
context,
otherUser,
40.0,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: otherUser.firstName,
lastName: otherUser.lastName,
imageUrl:
otherUser.imageUrl != null || otherUser.imageUrl != ""
? otherUser.imageUrl
: null,
),
size: 40.0,
),
title: otherUser.fullname ?? translations.anonymousUser,
subTitle: data != null
? data.isTextMessage
? data.text
: "📷 "
"${translations.image}"
: "",
lastUsed: chat.lastUsed != null
? dateFormatter.format(
date: chat.lastUsed!,
)
: null,
);
},
);
},
);
}
}
Future<bool?> _deleteDialog(
ChatModel chat,
ChatTranslations translations,
BuildContext context,
) async {
var theme = Theme.of(context);
var scope = ChatScope.of(context);
var options = scope.options;
return showModalBottomSheet<bool>(
context: context,
builder: (BuildContext context) => Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
translations.deleteChatModalTitle,
textAlign: TextAlign.center,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 20,
),
Text(
translations.deleteChatModalDescription,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium,
),
const SizedBox(
height: 20,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: CustomSemantics(
identifier: options.semantics.chatsDeleteConfirmButton,
child: FilledButton(
onPressed: () {
Navigator.of(
context,
).pop(true);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
translations.deleteChatModalConfirm,
style: theme.textTheme.displayLarge,
),
],
),
),
),
),
],
),
),
);
}
class _ChatRow extends StatelessWidget {
const _ChatRow({
required this.title,
required this.semanticIdTitle,
required this.semanticIdSubTitle,
required this.semanticIdLastUsed,
required this.semanticIdUnreadMessages,
this.unreadMessages = 0,
this.lastUsed,
this.subTitle,
this.avatar,
});
/// The title of the chat.
final String title;
final String semanticIdTitle;
/// The number of unread messages in the chat.
final int unreadMessages;
final String semanticIdUnreadMessages;
/// The last time the chat was used.
final String? lastUsed;
final String semanticIdLastUsed;
/// The subtitle of the chat.
final String? subTitle;
final String semanticIdSubTitle;
/// The avatar associated with the chat.
final Widget? avatar;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 10.0),
child: avatar,
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomSemantics(
identifier: semanticIdTitle,
value: title,
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium,
),
),
if (subTitle != null) ...[
Padding(
padding: const EdgeInsets.only(top: 3.0),
child: CustomSemantics(
identifier: semanticIdSubTitle,
value: subTitle,
child: Text(
subTitle!,
style: theme.textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
),
],
],
),
),
),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (lastUsed != null) ...[
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: CustomSemantics(
identifier: semanticIdLastUsed,
value: lastUsed,
child: Text(
lastUsed!,
style: theme.textTheme.labelSmall,
),
),
),
],
if (unreadMessages > 0) ...[
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
child: Center(
child: CustomSemantics(
identifier: semanticIdUnreadMessages,
value: unreadMessages.toString(),
child: Text(
unreadMessages.toString(),
style: const TextStyle(
fontSize: 14,
),
),
),
),
),
],
],
),
],
);
}
}

View file

@ -1,259 +0,0 @@
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/config/screen_types.dart";
import "package:flutter_chat/src/screens/creation/widgets/search_field.dart";
import "package:flutter_chat/src/screens/creation/widgets/search_icon.dart";
import "package:flutter_chat/src/screens/creation/widgets/user_list.dart";
import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_hooks/flutter_hooks.dart";
/// New chat screen
/// This screen is used to create a new chat
class NewChatScreen extends StatefulHookWidget {
/// Constructs a [NewChatScreen]
const NewChatScreen({
required this.onExit,
required this.onPressCreateGroupChat,
required this.onPressCreateChat,
super.key,
});
/// Callback function triggered when the create group chat button is pressed
final VoidCallback onPressCreateGroupChat;
/// Callback function triggered when a user is tapped
final Function(UserModel) onPressCreateChat;
/// Callback for when the user wants to navigate back
final VoidCallback onExit;
@override
State<NewChatScreen> createState() => _NewChatScreenState();
}
class _NewChatScreenState extends State<NewChatScreen> {
final FocusNode _textFieldFocusNode = FocusNode();
bool _isSearching = false;
String query = "";
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
useEffect(() {
chatScope.popHandler.add(widget.onExit);
return () => chatScope.popHandler.remove(widget.onExit);
});
if (options.builders.baseScreenBuilder == null) {
return Scaffold(
appBar: _AppBar(
isSearching: _isSearching,
onSearch: (query) {
setState(() {
_isSearching = query.isNotEmpty;
this.query = query;
});
},
onPressedSearchIcon: () {
setState(() {
_isSearching = !_isSearching;
query = "";
});
if (_isSearching) {
_textFieldFocusNode.requestFocus();
}
},
focusNode: _textFieldFocusNode,
),
body: _Body(
isSearching: _isSearching,
onPressCreateGroupChat: widget.onPressCreateGroupChat,
onPressCreateChat: widget.onPressCreateChat,
query: query,
),
);
}
return options.builders.baseScreenBuilder!.call(
context,
widget.mapScreenType,
_AppBar(
isSearching: _isSearching,
onSearch: (query) {
setState(() {
_isSearching = query.isNotEmpty;
this.query = query;
});
},
onPressedSearchIcon: () {
setState(() {
_isSearching = !_isSearching;
query = "";
});
if (_isSearching) {
_textFieldFocusNode.requestFocus();
}
},
focusNode: _textFieldFocusNode,
),
options.translations.newChatTitle,
_Body(
isSearching: _isSearching,
onPressCreateGroupChat: widget.onPressCreateGroupChat,
onPressCreateChat: widget.onPressCreateChat,
query: query,
),
);
}
}
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
const _AppBar({
required this.isSearching,
required this.onSearch,
required this.onPressedSearchIcon,
required this.focusNode,
});
final bool isSearching;
final Function(String) onSearch;
final VoidCallback onPressedSearchIcon;
final FocusNode focusNode;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var theme = Theme.of(context);
return AppBar(
iconTheme: theme.appBarTheme.iconTheme ??
const IconThemeData(color: Colors.white),
title: SearchField(
isSearching: isSearching,
onSearch: onSearch,
focusNode: focusNode,
text: options.translations.newChatTitle,
semanticId: options.semantics.newChatSearchInput,
),
actions: [
SearchIcon(
isSearching: isSearching,
onPressed: onPressedSearchIcon,
semanticId: options.semantics.newChatSearchIconButton,
),
],
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _Body extends StatelessWidget {
const _Body({
required this.isSearching,
required this.onPressCreateGroupChat,
required this.onPressCreateChat,
required this.query,
});
final bool isSearching;
final String query;
final VoidCallback onPressCreateGroupChat;
final Function(UserModel) onPressCreateChat;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var service = chatScope.service;
var options = chatScope.options;
var userId = chatScope.userId;
var translations = options.translations;
var theme = Theme.of(context);
return Column(
children: [
if (options.groupChatEnabled && !isSearching) ...[
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
top: 20,
),
child: CustomSemantics(
identifier: options.semantics.newChatCreateGroupChatButton,
child: FilledButton(
onPressed: onPressCreateGroupChat,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.groups_2,
),
const SizedBox(
width: 8,
),
Text(
translations.newGroupChatButton,
style: theme.textTheme.displayLarge,
),
],
),
),
),
),
],
Expanded(
child: StreamBuilder<List<UserModel>>(
// ignore: discarded_futures
stream: service.getAllUsers(),
builder: (context, snapshot) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return CustomSemantics(
identifier: options.semantics.newChatGetUsersError,
value: "Error: ${snapshot.error}",
child: Text("Error: ${snapshot.error}"),
);
} else if (snapshot.hasData) {
return UserList(
users: snapshot.data!,
currentUser: userId,
query: query,
onPressCreateChat: onPressCreateChat,
);
} else {
return options.builders.noUsersPlaceholderBuilder
?.call(context, translations) ??
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Align(
alignment: Alignment.topCenter,
child: Text(
translations.noUsersFound,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall,
),
),
);
}
},
),
),
],
);
}
}

View file

@ -1,454 +0,0 @@
import "dart:typed_data";
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/config/screen_types.dart";
import "package:flutter_chat/src/screens/creation/widgets/default_image_picker.dart";
import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:flutter_profile/flutter_profile.dart";
/// New group chat overview
/// Seen after the user has selected the users they
/// want to add to the group chat
class NewGroupChatOverview extends HookWidget {
/// Constructs a [NewGroupChatOverview]
const NewGroupChatOverview({
required this.onExit,
required this.users,
required this.onComplete,
super.key,
});
/// The users to be added to the group chat
final List<UserModel> users;
/// Callback for when the user wants to navigate back
final VoidCallback onExit;
/// Callback function triggered when the group chat is created
final Function(
List<UserModel> users,
String chatName,
String description,
Uint8List? image,
) onComplete;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
useEffect(() {
chatScope.popHandler.add(onExit);
return () => chatScope.popHandler.remove(onExit);
});
if (options.builders.baseScreenBuilder == null) {
return Scaffold(
appBar: const _AppBar(),
body: _Body(
users: users,
onComplete: onComplete,
),
);
}
return options.builders.baseScreenBuilder!.call(
context,
mapScreenType,
const _AppBar(),
options.translations.newGroupChatTitle,
_Body(
users: users,
onComplete: onComplete,
),
);
}
}
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
const _AppBar();
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var theme = Theme.of(context);
return AppBar(
iconTheme: theme.appBarTheme.iconTheme ??
const IconThemeData(color: Colors.white),
backgroundColor: theme.appBarTheme.backgroundColor,
title: Text(
options.translations.newGroupChatTitle,
),
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _Body extends StatefulWidget {
const _Body({
required this.users,
required this.onComplete,
});
final List<UserModel> users;
final Function(
List<UserModel> users,
String chatName,
String description,
Uint8List? image,
) onComplete;
@override
State<_Body> createState() => _BodyState();
}
class _BodyState extends State<_Body> {
final TextEditingController _chatNameController = TextEditingController();
final TextEditingController _bioController = TextEditingController();
final ValueNotifier<bool> isButtonEnabled = ValueNotifier<bool>(false);
Uint8List? image;
GlobalKey<FormState> formKey = GlobalKey<FormState>();
bool isPressed = false;
List<UserModel> users = <UserModel>[];
@override
void initState() {
users = widget.users;
super.initState();
_chatNameController.addListener(() {
isButtonEnabled.value = _chatNameController.text.isNotEmpty;
});
}
@override
void dispose() {
_chatNameController.dispose();
_bioController.dispose();
isButtonEnabled.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var theme = Theme.of(context);
var translations = options.translations;
return Stack(
children: [
SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Form(
key: formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 40,
),
Center(
child: Stack(
children: [
CustomSemantics(
identifier: options.semantics.newGroupChatSelectImage,
child: InkWell(
onTap: () async => onPressSelectImage(
context,
options,
(image) {
setState(() {
this.image = image;
});
},
),
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFFD9D9D9),
borderRadius: BorderRadius.circular(40),
image: image != null
? DecorationImage(
image: MemoryImage(image!),
fit: BoxFit.cover,
)
: null,
),
child: image == null
? const Icon(Icons.image)
: null,
),
),
),
if (image != null)
Positioned.directional(
textDirection: Directionality.of(context),
end: 0,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: const Color(0xFFBCBCBC),
borderRadius: BorderRadius.circular(40),
),
child: Center(
child: CustomSemantics(
identifier:
options.semantics.newGroupChatRemoveImage,
child: InkWell(
onTap: () {
setState(() {
image = null;
});
},
child: const Icon(
Icons.close,
size: 12,
),
),
),
),
),
)
else
const SizedBox.shrink(),
],
),
),
const SizedBox(
height: 40,
),
Text(
translations.groupChatNameFieldHeader,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 12,
),
CustomSemantics(
identifier: options.semantics.newChatNameInput,
isTextField: true,
child: TextFormField(
style: theme.textTheme.bodySmall,
controller: _chatNameController,
decoration: InputDecoration(
fillColor: Colors.white,
filled: true,
hintText: translations.groupNameHintText,
hintStyle: theme.textTheme.bodyMedium,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.transparent,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.transparent,
),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return translations.groupNameValidatorEmpty;
}
if (value.length > 15) {
return translations.groupNameValidatorTooLong;
}
return null;
},
),
),
const SizedBox(
height: 16,
),
Text(
translations.groupBioFieldHeader,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 12,
),
CustomSemantics(
identifier: options.semantics.newChatBioInput,
isTextField: true,
child: TextFormField(
style: theme.textTheme.bodySmall,
controller: _bioController,
minLines: null,
maxLines: 5,
decoration: InputDecoration(
fillColor: Colors.white,
filled: true,
hintText: translations.groupBioHintText,
hintStyle: theme.textTheme.bodyMedium,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.transparent,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.transparent,
),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return translations.groupBioValidatorEmpty;
}
return null;
},
),
),
const SizedBox(
height: 16,
),
CustomSemantics(
identifier: options.semantics.newGroupChatMemberAmount,
value: "${translations.selectedMembersHeader}"
"${users.length}",
child: Text(
"${translations.selectedMembersHeader}"
"${users.length}",
style: theme.textTheme.titleMedium,
),
),
const SizedBox(
height: 12,
),
Wrap(
children: [
...users.map(
(e) => _SelectedUser(
user: e,
onRemove: (user) {
setState(() {
users.remove(user);
});
},
),
),
],
),
const SizedBox(
height: 80,
),
],
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 80,
),
child: ValueListenableBuilder(
valueListenable: isButtonEnabled,
builder: (context, isEnabled, child) => CustomSemantics(
identifier: "",
child: FilledButton(
onPressed: users.isNotEmpty
? () async {
if (!isPressed) {
isPressed = true;
if (formKey.currentState!.validate()) {
await widget.onComplete(
users,
_chatNameController.text,
_bioController.text,
image,
);
}
isPressed = false;
}
}
: null,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
translations.createGroupChatButton,
style: theme.textTheme.displayLarge,
),
],
),
),
),
),
),
),
],
);
}
}
class _SelectedUser extends StatelessWidget {
const _SelectedUser({
required this.user,
required this.onRemove,
});
final UserModel user;
final Function(UserModel) onRemove;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
return CustomSemantics(
identifier: options.semantics.newGroupChatRemoveUser,
child: InkWell(
onTap: () {
onRemove(user);
},
child: Stack(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: options.builders.userAvatarBuilder?.call(
context,
user,
40,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl: user.imageUrl != "" ? user.imageUrl : null,
),
size: 40,
),
),
Positioned.directional(
textDirection: Directionality.of(context),
end: 0,
child: const Icon(
Icons.cancel,
size: 20,
),
),
],
),
),
);
}
}

View file

@ -1,298 +0,0 @@
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/config/screen_types.dart";
import "package:flutter_chat/src/screens/creation/widgets/search_field.dart";
import "package:flutter_chat/src/screens/creation/widgets/search_icon.dart";
import "package:flutter_chat/src/screens/creation/widgets/user_list.dart";
import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_hooks/flutter_hooks.dart";
/// New group chat screen
/// This screen is used to create a new group chat
class NewGroupChatScreen extends StatefulHookWidget {
/// Constructs a [NewGroupChatScreen]
const NewGroupChatScreen({
required this.onExit,
required this.onContinue,
super.key,
});
/// Callback for when the user wants to navigate back
final VoidCallback onExit;
/// Callback function triggered when the continue button is pressed
final Function(List<UserModel>) onContinue;
@override
State<NewGroupChatScreen> createState() => _NewGroupChatScreenState();
}
class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
final FocusNode _textFieldFocusNode = FocusNode();
bool _isSearching = false;
String query = "";
List<UserModel> selectedUsers = [];
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
useEffect(() {
chatScope.popHandler.add(widget.onExit);
return () => chatScope.popHandler.remove(widget.onExit);
});
if (options.builders.baseScreenBuilder == null) {
return Scaffold(
appBar: _AppBar(
isSearching: _isSearching,
onSearch: (query) {
setState(() {
_isSearching = query.isNotEmpty;
this.query = query;
});
},
onPressedSearchIcon: () {
setState(() {
_isSearching = !_isSearching;
query = "";
});
if (_isSearching) {
_textFieldFocusNode.requestFocus();
}
},
focusNode: _textFieldFocusNode,
),
body: _Body(
onSelectedUser: handleUserTap,
selectedUsers: selectedUsers,
onPressGroupChatOverview: widget.onContinue,
isSearching: _isSearching,
query: query,
),
);
}
return options.builders.baseScreenBuilder!.call(
context,
widget.mapScreenType,
_AppBar(
isSearching: _isSearching,
onSearch: (query) {
setState(() {
_isSearching = query.isNotEmpty;
this.query = query;
});
},
onPressedSearchIcon: () {
setState(() {
_isSearching = !_isSearching;
query = "";
});
if (_isSearching) {
_textFieldFocusNode.requestFocus();
}
},
focusNode: _textFieldFocusNode,
),
options.translations.newGroupChatTitle,
_Body(
onSelectedUser: handleUserTap,
selectedUsers: selectedUsers,
onPressGroupChatOverview: widget.onContinue,
isSearching: _isSearching,
query: query,
),
);
}
void handleUserTap(UserModel user) {
if (selectedUsers.contains(user)) {
setState(() {
selectedUsers.remove(user);
});
} else {
setState(() {
selectedUsers.add(user);
});
}
}
}
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
const _AppBar({
required this.isSearching,
required this.onSearch,
required this.onPressedSearchIcon,
required this.focusNode,
});
final bool isSearching;
final Function(String) onSearch;
final VoidCallback onPressedSearchIcon;
final FocusNode focusNode;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var theme = Theme.of(context);
return AppBar(
iconTheme: theme.appBarTheme.iconTheme ??
const IconThemeData(color: Colors.white),
title: SearchField(
isSearching: isSearching,
onSearch: onSearch,
focusNode: focusNode,
text: options.translations.newGroupChatTitle,
semanticId: options.semantics.newGroupChatSearchInput,
),
actions: [
SearchIcon(
isSearching: isSearching,
onPressed: onPressedSearchIcon,
semanticId: options.semantics.newGroupChatSearchIconButton,
),
],
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _Body extends StatelessWidget {
const _Body({
required this.isSearching,
required this.query,
required this.selectedUsers,
required this.onSelectedUser,
required this.onPressGroupChatOverview,
});
final bool isSearching;
final String query;
final List<UserModel> selectedUsers;
final Function(UserModel) onSelectedUser;
final Function(List<UserModel>) onPressGroupChatOverview;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var service = chatScope.service;
var options = chatScope.options;
var userId = chatScope.userId;
var translations = options.translations;
var theme = Theme.of(context);
return Column(
children: [
Expanded(
child: StreamBuilder<List<UserModel>>(
// ignore: discarded_futures
stream: service.getAllUsers(),
builder: (context, snapshot) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Semantics(
identifier: options.semantics.newGroupChatGetUsersError,
value: "Error: ${snapshot.error}",
child: Text("Error: ${snapshot.error}"),
);
} else if (snapshot.hasData) {
return Stack(
children: [
UserList(
users: snapshot.data!,
currentUser: userId,
query: query,
onPressCreateChat: null,
creatingGroup: true,
selectedUsers: selectedUsers,
onSelectedUser: onSelectedUser,
),
_NextButton(
selectedUsers: selectedUsers,
onPressGroupChatOverview: onPressGroupChatOverview,
),
],
);
} else {
return options.builders.noUsersPlaceholderBuilder
?.call(context, translations) ??
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Align(
alignment: Alignment.topCenter,
child: Text(
translations.noUsersFound,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall,
),
),
);
}
},
),
),
],
);
}
}
class _NextButton extends StatelessWidget {
const _NextButton({
required this.onPressGroupChatOverview,
required this.selectedUsers,
});
final Function(List<UserModel>) onPressGroupChatOverview;
final List<UserModel> selectedUsers;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var theme = Theme.of(context);
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 80,
),
child: Visibility(
visible: selectedUsers.isNotEmpty,
child: CustomSemantics(
identifier: options.semantics.newGroupChatNextButton,
child: FilledButton(
onPressed: () async {
await onPressGroupChatOverview(selectedUsers);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
options.translations.next,
style: theme.textTheme.displayLarge,
),
],
),
),
),
),
),
);
}
}

View file

@ -1,88 +0,0 @@
import "dart:io";
import "package:flutter/foundation.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/config/chat_options.dart";
import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_image_picker/flutter_image_picker.dart";
/// The function to call when the user selects an image
Future<void> onPressSelectImage(
BuildContext context,
ChatOptions options,
Function(Uint8List image) onUploadImage,
) async {
var image = await options.builders.imagePickerBuilder.call(context);
if (image == null) return;
await onUploadImage(image);
}
/// Default image picker dialog for selecting an image from the gallery or
/// taking a photo.
class DefaultImagePickerDialog extends StatelessWidget {
/// Creates a new default image picker dialog.
const DefaultImagePickerDialog({
super.key,
});
/// Builds the default image picker dialog.
static Future<Uint8List?> builder(BuildContext context) async =>
showModalBottomSheet<Uint8List?>(
context: context,
builder: (context) => const DefaultImagePickerDialog(),
);
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var translations = options.translations;
var theme = Theme.of(context);
var textTheme = theme.textTheme;
return options.builders.imagePickerContainerBuilder?.call(
context,
() => Navigator.of(context).pop(),
translations,
) ??
Container(
padding: const EdgeInsets.all(8.0),
color: Colors.white,
child: ImagePicker(
config: ImagePickerConfig(
imageQuality: options.imageQuality.clamp(0, 100),
cameraOption: !kIsWeb && (Platform.isAndroid || Platform.isIOS),
),
theme: ImagePickerTheme(
spaceBetweenIcons: 32.0,
iconColor: theme.primaryColor,
title: translations.imagePickerTitle,
titleStyle: textTheme.titleMedium,
iconSize: 60.0,
makePhotoText: translations.takePicture,
selectImageText: translations.uploadFile,
selectImageIcon: Icon(
color: theme.primaryColor,
Icons.insert_drive_file_rounded,
size: 60,
),
closeButtonBuilder: (ontap) => CustomSemantics(
identifier: options.semantics.imagePickerCancelButton,
child: TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
translations.cancelImagePickerBtn,
style: textTheme.bodyMedium!.copyWith(
fontSize: 18,
decoration: TextDecoration.underline,
),
),
),
),
),
),
);
}
}

View file

@ -1,65 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/util/scope.dart";
/// The search field widget
class SearchField extends StatelessWidget {
/// Constructs a [SearchField]
const SearchField({
required this.isSearching,
required this.onSearch,
required this.focusNode,
required this.text,
required this.semanticId,
super.key,
});
/// Whether the search field is currently in use
final bool isSearching;
/// Callback function triggered when the search field is used
final Function(String query) onSearch;
/// The focus node of the search field
final FocusNode focusNode;
/// The text to display in the search field
final String text;
/// Semantic id for search field
final String semanticId;
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var theme = Theme.of(context);
var translations = options.translations;
if (isSearching) {
return CustomSemantics(
identifier: semanticId,
isTextField: true,
child: TextField(
focusNode: focusNode,
onChanged: onSearch,
decoration: InputDecoration(
hintText: translations.searchPlaceholder,
hintStyle: theme.textTheme.bodyMedium,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: theme.colorScheme.primary,
),
),
),
style: theme.textTheme.bodySmall,
cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white,
),
);
}
return Text(
text,
);
}
}

View file

@ -1,36 +0,0 @@
import "package:flutter/material.dart";
/// A widget representing a search icon.
class SearchIcon extends StatelessWidget {
/// Constructs a [SearchIcon].
const SearchIcon({
required this.isSearching,
required this.onPressed,
required this.semanticId,
super.key,
});
/// Whether the search icon is currently in use
final bool isSearching;
/// Callback function triggered when the search icon is pressed
final VoidCallback onPressed;
/// Semantic id for icon button
final String semanticId;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Semantics(
identifier: semanticId,
child: IconButton(
onPressed: onPressed,
icon: Icon(
isSearching ? Icons.close : Icons.search,
color: theme.appBarTheme.iconTheme?.color ?? Colors.white,
),
),
);
}
}

View file

@ -1,216 +0,0 @@
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_accessibility/flutter_accessibility.dart";
import "package:flutter_chat/src/util/scope.dart";
import "package:flutter_profile/flutter_profile.dart";
/// The user list widget
class UserList extends StatefulWidget {
/// Constructs a [UserList]
const UserList({
required this.users,
required this.currentUser,
required this.query,
required this.onPressCreateChat,
this.creatingGroup = false,
this.selectedUsers = const [],
this.onSelectedUser,
super.key,
});
/// The list of users
final List<UserModel> users;
/// The query to search for
final String query;
/// The current user
final String currentUser;
/// Whether the user is creating a group
final bool creatingGroup;
/// Callback function triggered when a chat is created
final Function(UserModel)? onPressCreateChat;
/// The selected users
final List<UserModel> selectedUsers;
/// Callback function triggered when a user is selected
final Function(UserModel)? onSelectedUser;
@override
State<UserList> createState() => _UserListState();
}
class _UserListState extends State<UserList> {
List<UserModel> users = [];
List<UserModel> filteredUsers = [];
bool isPressed = false;
@override
void initState() {
super.initState();
users = List.from(widget.users);
users.removeWhere((user) => user.id == widget.currentUser);
}
@override
Widget build(BuildContext context) {
var chatScope = ChatScope.of(context);
var options = chatScope.options;
var theme = Theme.of(context);
var translations = options.translations;
filteredUsers = widget.query.isNotEmpty
? users
.where(
(user) =>
user.fullname?.toLowerCase().contains(
widget.query.toLowerCase(),
) ??
false,
)
.toList()
: users;
return Padding(
padding: const EdgeInsets.only(top: 8, left: 12, right: 12, bottom: 80),
child: ListView.builder(
itemCount: filteredUsers.length,
itemBuilder: (context, index) {
var user = filteredUsers[index];
var isSelected = widget.selectedUsers.any((u) => u.id == user.id);
return CustomSemantics(
identifier: options.semantics.userListTapUser(index),
buttonWithVariableText: true,
child: InkWell(
onTap: () async {
if (widget.creatingGroup) {
return handleGroupChatTap(user);
} else {
return handlePersonalChatTap(user);
}
},
child: options.builders.chatRowContainerBuilder?.call(
context,
Row(
children: [
options.builders.userAvatarBuilder
?.call(context, user, 44) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl:
user.imageUrl != "" ? user.imageUrl : null,
),
size: 44,
),
const SizedBox(
width: 12,
),
CustomSemantics(
identifier: options.semantics
.newChatUserListUserFullName(index),
value: user.fullname ?? translations.anonymousUser,
child: Text(
user.fullname ?? translations.anonymousUser,
style: theme.textTheme.titleMedium,
),
),
if (widget.creatingGroup) ...[
const Spacer(),
Checkbox(
value: isSelected,
onChanged: (value) {
handleGroupChatTap(user);
},
),
const SizedBox(
width: 12,
),
],
],
),
) ??
DecoratedBox(
decoration: BoxDecoration(
color: Colors.transparent,
border: Border(
bottom: BorderSide(
color: theme.dividerColor,
width: 0.5,
),
),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
options.builders.userAvatarBuilder
?.call(context, user, 44) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl: user.imageUrl != ""
? user.imageUrl
: null,
),
size: 44,
),
const SizedBox(
width: 12,
),
CustomSemantics(
identifier: options.semantics
.newChatUserListUserFullName(index),
value: user.fullname ?? translations.anonymousUser,
child: Text(
user.fullname ?? translations.anonymousUser,
style: theme.textTheme.titleMedium,
),
),
if (widget.creatingGroup) ...[
const Spacer(),
Checkbox(
value: isSelected,
onChanged: (value) {
handleGroupChatTap(user);
},
),
const SizedBox(
width: 12,
),
],
],
),
),
),
),
);
},
),
);
}
Future<void> handlePersonalChatTap(UserModel user) async {
if (!isPressed) {
setState(() {
isPressed = true;
});
await widget.onPressCreateChat?.call(user);
setState(() {
isPressed = false;
});
}
}
void handleGroupChatTap(UserModel user) {
widget.onSelectedUser?.call(user);
}
}

View file

@ -1,74 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter_chat/src/config/chat_options.dart";
import "package:intl/intl.dart";
/// The date formatter
class DateFormatter {
/// Constructs a [DateFormatter]
DateFormatter({
required this.options,
});
/// The chat options
final ChatOptions options;
final _now = DateTime.now();
bool _isToday(DateTime date) =>
DateTime(
date.year,
date.month,
date.day,
)
.difference(
DateTime(
_now.year,
_now.month,
_now.day,
),
)
.inDays ==
0;
bool _isYesterday(DateTime date) =>
DateTime(
date.year,
date.month,
date.day,
)
.difference(
DateTime(
_now.year,
_now.month,
_now.day,
),
)
.inDays ==
-1;
bool _isThisYear(DateTime date) => date.year == _now.year;
/// Formats the date
String format({
required DateTime date,
bool showFullDate = false,
}) {
if (options.dateformat != null) {
return options.dateformat!(showFullDate, date);
}
if (_isToday(date)) {
return DateFormat(
"HH:mm",
).format(date);
} else if (_isYesterday(date)) {
return "yesterday";
} else if (_isThisYear(date)) {
return DateFormat("dd-MM${showFullDate ? " HH:mm" : ""}").format(date);
} else {
return DateFormat("dd-MM-yyyy${showFullDate ? " HH:mm" : ""}")
.format(date);
}
}
}

View file

@ -1,24 +0,0 @@
import "package:flutter/material.dart";
///
class PopHandler {
/// Constructor
PopHandler();
final List<VoidCallback> _handlers = [];
/// Registers a new handler
void add(VoidCallback handler) {
_handlers.add(handler);
}
/// Removes a handler
void remove(VoidCallback handler) {
_handlers.remove(handler);
}
/// Handles the pop
void handlePop() {
_handlers.lastOrNull?.call();
}
}

View file

@ -1,37 +0,0 @@
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/widgets.dart";
import "package:flutter_chat/src/config/chat_options.dart";
import "package:flutter_chat/src/services/pop_handler.dart";
///
class ChatScope extends InheritedWidget {
///
const ChatScope({
required this.userId,
required this.options,
required this.service,
required this.popHandler,
required super.child,
super.key,
});
///
final String userId;
///
final ChatOptions options;
///
final ChatService service;
///
final PopHandler popHandler;
@override
bool updateShouldNotify(ChatScope oldWidget) =>
oldWidget.userId != userId || oldWidget.options != options;
///
static ChatScope of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<ChatScope>()!;
}

View file

@ -1,18 +0,0 @@
// add generic utils that are used in the package
/// Extension to simplify detecting how many days relative dates are
extension RelativeDates on DateTime {
/// Strips timezone information whilst keeping the exact same date
DateTime get utcDate => DateTime.utc(year, month, day);
/// Strips time information from the date
DateTime get date => DateTime(year, month, day);
/// Get relative date in offset from the current position.
///
/// `today.getDateOffsetInDays(yesterday)` would result in `-1`
///
/// `yesterday.getDateOffsetInDays(tomorrow)` would result in `2`
int getDateOffsetInDays(DateTime other) =>
other.utcDate.difference(utcDate).inDays;
}

View file

@ -1,37 +0,0 @@
name: flutter_chat
description: "User story of the chat domain for quick integration into flutter apps"
version: 6.0.0
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
environment:
sdk: ">=3.4.3 <4.0.0"
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
cached_network_image: ^3.2.2
intl: any
flutter_hooks: ^0.20.5
flutter_accessibility:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^0.0.2
flutter_image_picker:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^4.0.0
flutter_profile:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^1.6.0
chat_repository_interface:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^6.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0

View file

@ -1,14 +0,0 @@
import "package:flutter_chat/src/util/utils.dart";
import "package:flutter_test/flutter_test.dart";
void main() {
group("RelativeDates", () {
test("getDateOffsetInDays", () {
var dateA = DateTime(2024, 10, 30);
var dateB = DateTime(2024, 10, 01);
expect(dateA.getDateOffsetInDays(dateB), equals(29));
expect(dateB.getDateOffsetInDays(dateA), equals(-29));
});
});
}

View file

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 18a827f3933c19f51862dde3fa472197683249d6
channel: stable
project_type: package

View file

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

View file

@ -0,0 +1 @@
TODO: Add your license here.

View file

@ -0,0 +1,39 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
## Features
TODO: List what your package can do. Maybe include images, gifs, or videos.
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
```dart
const like = 'sample';
```
## Additional information
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.

View file

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -0,0 +1,8 @@
// 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';

View file

@ -0,0 +1,582 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "50.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "5.2.0"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.1"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.10.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
build:
dependency: transitive
description:
name: build
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.1"
built_collection:
dependency: transitive
description:
name: built_collection
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
version: "8.4.2"
cached_network_image:
dependency: transitive
description:
name: cached_network_image
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.3"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
code_builder:
dependency: transitive
description:
name: code_builder
url: "https://pub.dartlang.org"
source: hosted
version: "4.3.0"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.16.0"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.1"
cross_file:
dependency: transitive
description:
name: cross_file
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3+2"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.2"
dart_style:
dependency: transitive
description:
name: dart_style
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.4"
ffi:
dependency: transitive
description:
name: ffi
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.4"
fixnum:
dependency: transitive
description:
name: fixnum
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_blurhash:
dependency: transitive
description:
name: flutter_blurhash
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.0"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.0"
flutter_community_chat_interface:
dependency: "direct main"
description:
path: "packages/flutter_community_chat_interface"
ref: HEAD
resolved-ref: "2836392011995d4f3e96b333a82b28c26945c7d5"
url: "https://github.com/Iconica-Development/flutter_community_chat.git"
source: git
version: "0.0.1"
flutter_community_chat_view:
dependency: "direct main"
description:
path: "packages/flutter_community_chat_view"
ref: HEAD
resolved-ref: "2836392011995d4f3e96b333a82b28c26945c7d5"
url: "https://github.com/Iconica-Development/flutter_community_chat.git"
source: git
version: "0.0.1"
flutter_data_interface:
dependency: transitive
description:
path: "."
ref: master
resolved-ref: "500ed1d08095b33387ae3aa4ed1a2ad4d2fb2ac3"
url: "https://github.com/Iconica-Development/flutter_data_interface.git"
source: git
version: "1.0.0"
flutter_image_picker:
dependency: transitive
description:
path: "."
ref: "1.0.3"
resolved-ref: "20814755cca74296600a0ae3e016e46979e66a7e"
url: "https://github.com/Iconica-Development/flutter_image_picker"
source: git
version: "1.0.3"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
http:
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.5"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.2"
image_picker:
dependency: transitive
description:
name: image_picker
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.6"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.5+4"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.10"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.6+2"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.6.2"
intl:
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.4"
lints:
dependency: transitive
description:
name: lints
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
logging:
dependency: transitive
description:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.14"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.5"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
mockito:
dependency: transitive
description:
name: mockito
url: "https://pub.dartlang.org"
source: hosted
version: "5.3.2"
octo_image:
dependency: transitive
description:
name: octo_image
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.3"
path_provider:
dependency: transitive
description:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.22"
path_provider_ios:
dependency: transitive
description:
name: path_provider_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.7"
path_provider_macos:
dependency: transitive
description:
name: path_provider_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.1"
platform:
dependency: transitive
description:
name: platform
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
process:
dependency: transitive
description:
name: process
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.4"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
rxdart:
dependency: transitive
description:
name: rxdart
url: "https://pub.dartlang.org"
source: hosted
version: "0.27.7"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_gen:
dependency: transitive
description:
name: source_gen
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.6"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.1"
sqflite:
dependency: transitive
description:
name: sqflite
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.2"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0+2"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
synchronized:
dependency: transitive
description:
name: synchronized
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0+3"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.17"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
uuid:
dependency: transitive
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.7"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.3"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0+2"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.1"
sdks:
dart: ">=2.18.2 <3.0.0"
flutter: ">=3.3.0"

View file

@ -0,0 +1,61 @@
name: flutter_community_chat
description: A new Flutter package project.
version: 0.0.1
homepage:
publish_to: none
environment:
sdk: '>=2.18.2 <3.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
flutter_community_chat_view:
git:
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
dev_dependencies:
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# To add assets to your package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# To add custom fonts to your package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages

View file

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 18a827f3933c19f51862dde3fa472197683249d6
channel: stable
project_type: package

View file

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

View file

@ -0,0 +1 @@
TODO: Add your license here.

View file

@ -0,0 +1,39 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
## Features
TODO: List what your package can do. Maybe include images, gifs, or videos.
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
```dart
const like = 'sample';
```
## Additional information
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.

View file

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
class FirebaseChatOptions {
const FirebaseChatOptions({
this.chatsCollectionName = 'chats',
this.messagesCollectionName = 'messages',
this.usersCollectionName = 'users',
});
final String chatsCollectionName;
final String messagesCollectionName;
final String usersCollectionName;
}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_community_chat_firebase/dto/firebase_message_document.dart';
class FirebaseChatDocument {
FirebaseChatDocument({
required this.personal,
this.users = const [],
this.id,
this.lastUsed,
this.title,
this.lastMessage,
});
final String? id;
final String? title;
final bool personal;
final Timestamp? lastUsed;
final List<String> users;
final FirebaseMessageDocument? lastMessage;
FirebaseChatDocument.fromJson(Map<String, dynamic> json, this.id)
: title = json['title'],
personal = json['personal'],
lastUsed = json['last_used'],
users = List<String>.from(json['users']),
lastMessage = json['last_message'] == null
? null
: FirebaseMessageDocument.fromJson(
json['last_message'],
null,
);
Map<String, dynamic> toJson() => {
'title': title,
'personal': personal,
'last_used': lastUsed,
'users': users,
};
}

View file

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:cloud_firestore/cloud_firestore.dart';
class FirebaseMessageDocument {
FirebaseMessageDocument({
required this.sender,
required this.timestamp,
this.id,
this.text,
this.imageUrl,
});
final String? id;
final String sender;
final String? text;
final String? imageUrl;
final Timestamp timestamp;
FirebaseMessageDocument.fromJson(Map<String, dynamic> json, this.id)
: sender = json['sender'],
text = json['text'],
imageUrl = json['image_url'],
timestamp = json['timestamp'];
Map<String, dynamic> toJson() => {
'sender': sender,
'text': text,
'image_url': imageUrl,
'timestamp': timestamp,
};
}

View file

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
class FirebaseUserDocument {
FirebaseUserDocument({
this.firstName,
this.lastName,
this.imageUrl,
this.id,
});
final String? firstName;
final String? lastName;
final String? imageUrl;
final String? id;
FirebaseUserDocument.fromJson(
Map<String, Object?> json,
String id,
) : this(
id: id,
firstName:
json['first_name'] == null ? '' : json['first_name'] as String,
lastName:
json['last_name'] == null ? '' : json['last_name'] as String,
imageUrl:
json['image_url'] == null ? null : json['image_url'] as String);
Map<String, Object?> toJson() {
return {
'first_name': firstName,
'last_name': lastName,
'image_url': imageUrl,
};
}
}

View file

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

Some files were not shown because too many files have changed in this diff Show more