mirror of
https://github.com/Iconica-Development/flutter_chat.git
synced 2025-05-19 10:53:51 +02:00
Compare commits
241 commits
Author | SHA1 | Date | |
---|---|---|---|
f7f15ef750 | |||
c155531b11 | |||
|
90610caabd | ||
|
e1e23e7b35 | ||
9b365d573d | |||
3b4b456db2 | |||
|
ad615133e4 | ||
f286e7fb79 | |||
3f1caa912b | |||
7d634e54c1 | |||
|
3fbcf5d076 | ||
84cc630c6e | |||
|
3cec2ee1c6 | ||
|
b8e22425a1 | ||
|
61b588cfd5 | ||
|
02ae2aa884 | ||
|
d2f000c8a7 | ||
52562746b6 | |||
a48806fe98 | |||
bcf2c0484b | |||
4e0967cc33 | |||
|
c63efadd2c | ||
|
ad4cf1e37b | ||
|
d14ad4716a | ||
|
c1e20e84ff | ||
5126b8dab7 | |||
|
371ff6c335 | ||
|
b3b8b1828e | ||
|
30fc7b4368 | ||
6ecf073f15 | |||
d3f839dc94 | |||
|
ba818158fb | ||
|
c1181d4e84 | ||
|
b0b4121a25 | ||
|
313ead2029 | ||
|
5975e2f1c0 | ||
|
a1fc65aba2 | ||
|
11d8c81161 | ||
|
590a339d0d | ||
|
2508789a6d | ||
|
b0d379284d | ||
|
62f04e2d9b | ||
|
6a63429efd | ||
|
1a7b4a2cda | ||
|
c82df25aed | ||
|
23f61dd5ff | ||
e7bb4909ba | |||
|
02ae851d13 | ||
|
7a0fd49070 | ||
|
70eeb816e2 | ||
|
f57ba9a736 | ||
|
ab8a9d9e6f | ||
|
e1ca5aab71 | ||
|
8112e939e1 | ||
|
7c80341ff5 | ||
|
5d29e733aa | ||
|
d475cf7298 | ||
|
77d6f7257e | ||
|
990a89199b | ||
|
281188c2b7 | ||
|
8604ccada7 | ||
|
a9b52ef5d9 | ||
|
a9eb1a8df4 | ||
|
b1909689f2 | ||
|
4ec7da429e | ||
|
ff28f91524 | ||
|
d66942893f | ||
|
15604bf264 | ||
|
4c48cf8cd4 | ||
|
55be653975 | ||
|
ed72545cc4 | ||
|
070a4d5adc | ||
|
22884ea395 | ||
|
3e03dd755e | ||
|
7457602afe | ||
|
1ea2887e27 | ||
|
91420fde78 | ||
d5b7183df5 | |||
bd14f5cd6d | |||
|
b6fc7b2cb0 | ||
|
4ee5445809 | ||
|
ed95dbd15c | ||
|
f8bffceb4b | ||
61de7ae44a | |||
1f3dc09f44 | |||
ec89961e07 | |||
|
44579ca306 | ||
|
8f13d87a23 | ||
|
644615f026 | ||
|
9be096154f | ||
|
f5040d5809 | ||
c7fe7152b3 | |||
2564c74066 | |||
|
15f15748b6 | ||
|
2f2e2be6dd | ||
|
d46c83e847 | ||
|
d13a8013ac | ||
|
8b4ada7edc | ||
|
61d901c741 | ||
|
5e4a9c7ab4 | ||
|
1141aea83c | ||
|
5464766747 | ||
7cfd8087a1 | |||
|
3d3153d2ce | ||
|
c9a11758d8 | ||
|
82448ab9e0 | ||
|
efd6fc138c | ||
|
1eb5f99b7b | ||
|
146ec3a1a9 | ||
|
b5656d5f3a | ||
|
86c50f47f6 | ||
|
06167d202e | ||
|
37e975ceec | ||
|
58451a7e5e | ||
|
c370df0bdd | ||
|
0d22cea2f7 | ||
|
3b443ee1fc | ||
|
70c795e89a | ||
|
3de70d7710 | ||
|
3b7256173f | ||
|
e85b107038 | ||
|
6bafc86a2d | ||
|
19529deb4e | ||
|
c3b03f6d38 | ||
|
158973cd7a | ||
|
c4955e6eb7 | ||
|
f2e6875630 | ||
|
89edfdd18c | ||
|
6e2d0feac9 | ||
|
a59e149420 | ||
|
32189ba397 | ||
|
8e51a4d3fd | ||
|
169818e981 | ||
|
6d23ea04fa | ||
|
e9da9f98e3 | ||
|
0950eb3a17 | ||
|
1badf8a851 | ||
|
48668ab89d | ||
|
b0c8f17bcf | ||
|
6600a84781 | ||
|
baefa03a94 | ||
|
5052d016da | ||
|
37bac6c4cb | ||
|
a4d16c59b8 | ||
|
2a0011b9d1 | ||
|
c5af30349d | ||
|
5ae3295a8d | ||
|
ce033d7dbd | ||
|
fbb8d5235f | ||
|
3511db2f27 | ||
|
797eedc835 | ||
|
910cfc4fc6 | ||
|
a6550fd57a | ||
|
16a40b7451 | ||
|
5f890fe75f | ||
|
d3655e3395 | ||
|
da7d639e05 | ||
|
2b5ab5a933 | ||
|
28e307cf90 | ||
|
23b96e5ce3 | ||
|
f8ca89a762 | ||
|
37cd36484a | ||
|
cb423c582c | ||
|
ac163a28f8 | ||
|
69bafc33e6 | ||
|
4fd823511f | ||
|
4760e281ee | ||
|
b3b9ceb07d | ||
|
7e503bb0ce | ||
|
07e29ddd99 | ||
|
7b33ff2bd7 | ||
|
2c0bd42636 | ||
|
d90185a480 | ||
|
d0933ac252 | ||
|
d870b8424b | ||
|
fde5b289a6 | ||
|
19e539734e | ||
|
b7e5c51413 | ||
|
29d993682a | ||
|
08e695517b | ||
|
5d4aadc62e | ||
|
5e41f3885f | ||
|
f6a2a26def | ||
|
c3928c19f6 | ||
|
84934dec0c | ||
|
0d9e192134 | ||
|
96f3138656 | ||
|
2e79910e77 | ||
|
60bcd1c26a | ||
|
9b4ce62392 | ||
|
d4e42ac440 | ||
|
5b8c6db64f | ||
|
b7bb8c394a | ||
|
268eaadf82 | ||
|
689a295654 | ||
|
80b25113e1 | ||
|
05ec6448f8 | ||
|
67087ab147 | ||
|
6f78ae6518 | ||
|
3003ed5389 | ||
|
e3a63a9f4b | ||
|
be0a99a6df | ||
|
8f2b37c0aa | ||
|
6b32c3f55b | ||
|
2a0d3115e8 | ||
|
a00e5d81be | ||
|
fa6a8d8b23 | ||
|
35d89faf04 | ||
|
b99bdfd081 | ||
|
65d3a0b6b3 | ||
|
77da686859 | ||
|
bba48f33c9 | ||
676988bfd1 | |||
0898935345 | |||
|
659e2692f1 | ||
|
4df2adb984 | ||
|
f55c43653c | ||
|
e153b926c9 | ||
|
cf3cd503e5 | ||
|
35e1aab154 | ||
|
13f57a0817 | ||
|
3dde81838d | ||
|
f110c224a2 | ||
|
0caebe7d02 | ||
|
7fd38e3770 | ||
|
5e53049a3e | ||
|
d024a9fad9 | ||
|
942403d1a4 | ||
|
f800216a9b | ||
|
79d3de0b35 | ||
|
2ebb05bb79 | ||
|
9e0378fe74 | ||
|
bd40856e1a | ||
|
bae97e917e | ||
|
01ab5599d4 | ||
|
3480b97ba0 | ||
|
7b03c934cf | ||
|
2d36d02533 | ||
|
4380560593 | ||
dbb2ffb708 | |||
|
0b449627b8 |
216 changed files with 8857 additions and 6097 deletions
3
.fvmrc
Normal file
3
.fvmrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"flutter": "3.24.3"
|
||||
}
|
22
.github/dependabot.yml
vendored
Normal file
22
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "pub"
|
||||
directory: "/packages/flutter_chat"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "pub"
|
||||
directory: "/packages/flutter_chat_firebase"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "pub"
|
||||
directory: "/packages/flutter_chat_interface"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "pub"
|
||||
directory: "/packages/flutter_chat_view"
|
||||
schedule:
|
||||
interval: "weekly"
|
14
.github/workflows/component-documentation.yml
vendored
Normal file
14
.github/workflows/component-documentation.yml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
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
|
15
.github/workflows/melos-ci.yml
vendored
Normal file
15
.github/workflows/melos-ci.yml
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
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
|
15
.github/workflows/release.yml
vendored
Normal file
15
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
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
23
.gitignore
vendored
|
@ -19,6 +19,7 @@ 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
|
||||
|
@ -27,8 +28,28 @@ migrate_working_dir/
|
|||
|
||||
# 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
|
||||
|
|
172
CHANGELOG.md
172
CHANGELOG.md
|
@ -1,3 +1,175 @@
|
|||
## 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
|
||||
|
|
7
FEATURES.md
Normal file
7
FEATURES.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
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
|
9
LICENSE
Normal file
9
LICENSE
Normal file
|
@ -0,0 +1,9 @@
|
|||
Copyright (c) 2024 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:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
272
README.md
272
README.md
|
@ -1,66 +1,270 @@
|
|||
# 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.
|
||||
# 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.
|
||||
|
||||

|
||||
|
||||
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]
|
||||
|
||||
## Setup
|
||||
To use this package, add flutter_community_chat as a dependency in your pubspec.yaml file:
|
||||
|
||||
```
|
||||
flutter_community_chat:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_community_chat.git
|
||||
path: packages/flutter_community_chat
|
||||
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
|
||||
```
|
||||
|
||||
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:
|
||||
You can use the `LocalChatService` to test the package in your project:
|
||||
|
||||
```dart
|
||||
ChatUserStoryConfiguration(
|
||||
chatService: LocalChatService(),
|
||||
),
|
||||
```
|
||||
flutter_community_chat_firebase:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_community_chat.git
|
||||
path: packages/flutter_community_chat_firebase
|
||||
|
||||
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"/>
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
```
|
||||
CommunityChat(
|
||||
dataProvider: FirebaseCommunityChatDataProvider(),
|
||||
)
|
||||
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(),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
In this example we provide a `FirebaseCommunityChatDataProvider` as a data provider. You can also specify your own implementation here of the `CommunityChatInterface` interface.
|
||||
You can override any method in the `ChatUserStoryConfiguration`.
|
||||
|
||||
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:
|
||||
Add the `getChatRoutes()` to your go_router routes like so:
|
||||
|
||||
```dart
|
||||
final GoRouter _router = GoRouter(
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
return const MyHomePage(
|
||||
title: "home",
|
||||
);
|
||||
},
|
||||
),
|
||||
...getChatRoutes()
|
||||
],
|
||||
);
|
||||
```
|
||||
CommunityChat(
|
||||
dataProvider: FirebaseCommunityChatDataProvider(),
|
||||
imagePickerTheme: ImagePickerTheme(),
|
||||
chatOptions: ChatOptions(),
|
||||
)
|
||||
|
||||
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,
|
||||
);
|
||||
```
|
||||
|
||||
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_community_chat/pulls) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
|
||||
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_chat/pulls) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
|
||||
|
||||
## Want to contribute
|
||||
|
||||
If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](../CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_community_chat/pulls).
|
||||
If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_chat/pulls).
|
||||
|
||||
## Author
|
||||
|
||||
This `flutter_community_chat` for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>
|
||||
This `flutter_chat` for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>
|
||||
|
|
BIN
example.gif
Normal file
BIN
example.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 267 KiB |
25
melos.yaml
25
melos.yaml
|
@ -1,4 +1,4 @@
|
|||
name: flutter_community_chat
|
||||
name: flutter_chat
|
||||
|
||||
packages:
|
||||
- packages/**
|
||||
|
@ -9,22 +9,31 @@ command:
|
|||
|
||||
scripts:
|
||||
lint:all:
|
||||
run: melos run analyze && melos run format
|
||||
run: dart run melos run analyze && dart run melos run format-check
|
||||
description: Run all static analysis checks.
|
||||
|
||||
get:
|
||||
run: melos exec -c 1 -- "flutter pub get"
|
||||
run: |
|
||||
melos exec -c 1 -- "flutter pub get"
|
||||
melos exec --scope="*example*" -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: |
|
||||
melos exec -c 1 -- \
|
||||
dart run melos exec -c 1 -- \
|
||||
flutter analyze --fatal-infos
|
||||
description: Run `flutter analyze` for all packages.
|
||||
|
||||
format:
|
||||
run: melos exec flutter format . --fix
|
||||
description: Run `flutter format` for all packages.
|
||||
run: dart run melos exec dart format .
|
||||
description: Run `dart format` for all packages.
|
||||
|
||||
format-check:
|
||||
run: melos exec flutter format . --set-exit-if-changed
|
||||
description: Run `flutter format` checks for all packages.
|
||||
run: dart run melos exec dart format . --set-exit-if-changed
|
||||
description: Run `dart format` checks for all packages.
|
||||
|
|
29
packages/chat_repository_interface/.gitignore
vendored
Normal file
29
packages/chat_repository_interface/.gitignore
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
build/
|
1
packages/chat_repository_interface/CHANGELOG.md
Symbolic link
1
packages/chat_repository_interface/CHANGELOG.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../CHANGELOG.md
|
1
packages/chat_repository_interface/LICENSE
Symbolic link
1
packages/chat_repository_interface/LICENSE
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE
|
9
packages/chat_repository_interface/analysis_options.yaml
Normal file
9
packages/chat_repository_interface/analysis_options.yaml
Normal file
|
@ -0,0 +1,9 @@
|
|||
include: package:flutter_iconica_analysis/components_options.yaml
|
||||
|
||||
# Possible to overwrite the rules from the package
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
|
||||
linter:
|
||||
rules:
|
|
@ -0,0 +1,16 @@
|
|||
// 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";
|
|
@ -0,0 +1,23 @@
|
|||
/// 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;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
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";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
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,
|
||||
});
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
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,
|
||||
});
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
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});
|
||||
}
|
|
@ -0,0 +1,290 @@
|
|||
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;
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
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",
|
||||
),
|
||||
];
|
|
@ -0,0 +1,90 @@
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
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;
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/// 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);
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/// 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;
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/// 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";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,343 @@
|
|||
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);
|
||||
}
|
21
packages/chat_repository_interface/pubspec.yaml
Normal file
21
packages/chat_repository_interface/pubspec.yaml
Normal file
|
@ -0,0 +1,21 @@
|
|||
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
|
29
packages/firebase_chat_repository/.gitignore
vendored
Normal file
29
packages/firebase_chat_repository/.gitignore
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
build/
|
1
packages/firebase_chat_repository/CHANGELOG.md
Symbolic link
1
packages/firebase_chat_repository/CHANGELOG.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../CHANGELOG.md
|
1
packages/firebase_chat_repository/LICENSE
Symbolic link
1
packages/firebase_chat_repository/LICENSE
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE
|
16
packages/firebase_chat_repository/README.md
Normal file
16
packages/firebase_chat_repository/README.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
# 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',
|
||||
),
|
||||
),
|
||||
```
|
9
packages/firebase_chat_repository/analysis_options.yaml
Normal file
9
packages/firebase_chat_repository/analysis_options.yaml
Normal file
|
@ -0,0 +1,9 @@
|
|||
include: package:flutter_iconica_analysis/components_options.yaml
|
||||
|
||||
# Possible to overwrite the rules from the package
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
|
||||
linter:
|
||||
rules:
|
|
@ -0,0 +1,2 @@
|
|||
export "src/firebase_chat_repository.dart";
|
||||
export "src/firebase_user_repository.dart";
|
|
@ -0,0 +1,220 @@
|
|||
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 {}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
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(),
|
||||
);
|
||||
}
|
29
packages/firebase_chat_repository/pubspec.yaml
Normal file
29
packages/firebase_chat_repository/pubspec.yaml
Normal file
|
@ -0,0 +1,29 @@
|
|||
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
|
29
packages/flutter_chat/.gitignore
vendored
Normal file
29
packages/flutter_chat/.gitignore
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
build/
|
1
packages/flutter_chat/CHANGELOG.md
Symbolic link
1
packages/flutter_chat/CHANGELOG.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../CHANGELOG.md
|
1
packages/flutter_chat/LICENSE
Symbolic link
1
packages/flutter_chat/LICENSE
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE
|
9
packages/flutter_chat/analysis_options.yaml
Normal file
9
packages/flutter_chat/analysis_options.yaml
Normal file
|
@ -0,0 +1,9 @@
|
|||
include: package:flutter_iconica_analysis/components_options.yaml
|
||||
|
||||
# Possible to overwrite the rules from the package
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
|
||||
linter:
|
||||
rules:
|
|
@ -1,7 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2022 Iconica
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
|
@ -9,9 +5,11 @@
|
|||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
|
@ -31,7 +29,6 @@ migrate_working_dir/
|
|||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
@ -46,3 +43,5 @@ app.*.map.json
|
|||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
firebase_options.dart
|
16
packages/flutter_chat/example/README.md
Normal file
16
packages/flutter_chat/example/README.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
# 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.
|
28
packages/flutter_chat/example/analysis_options.yaml
Normal file
28
packages/flutter_chat/example/analysis_options.yaml
Normal file
|
@ -0,0 +1,28 @@
|
|||
# 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
|
1
packages/flutter_chat/example/firebase.json
Normal file
1
packages/flutter_chat/example/firebase.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"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"}}}}}
|
63
packages/flutter_chat/example/lib/main.dart
Normal file
63
packages/flutter_chat/example/lib/main.dart
Normal file
|
@ -0,0 +1,63 @@
|
|||
// 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',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
28
packages/flutter_chat/example/pubspec.yaml
Normal file
28
packages/flutter_chat/example/pubspec.yaml
Normal file
|
@ -0,0 +1,28 @@
|
|||
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
|
30
packages/flutter_chat/example/test/widget_test.dart
Normal file
30
packages/flutter_chat/example/test/widget_test.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
// 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);
|
||||
});
|
||||
}
|
31
packages/flutter_chat/lib/flutter_chat.dart
Normal file
31
packages/flutter_chat/lib/flutter_chat.dart
Normal file
|
@ -0,0 +1,31 @@
|
|||
// 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";
|
204
packages/flutter_chat/lib/src/config/chat_builders.dart
Normal file
204
packages/flutter_chat/lib/src/config/chat_builders.dart
Normal file
|
@ -0,0 +1,204 @@
|
|||
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,
|
||||
);
|
374
packages/flutter_chat/lib/src/config/chat_options.dart
Normal file
374
packages/flutter_chat/lib/src/config/chat_options.dart
Normal file
|
@ -0,0 +1,374 @@
|
|||
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;
|
||||
}
|
271
packages/flutter_chat/lib/src/config/chat_semantics.dart
Normal file
271
packages/flutter_chat/lib/src/config/chat_semantics.dart
Normal file
|
@ -0,0 +1,271 @@
|
|||
// 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";
|
|
@ -0,0 +1,105 @@
|
|||
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);
|
273
packages/flutter_chat/lib/src/config/chat_translations.dart
Normal file
273
packages/flutter_chat/lib/src/config/chat_translations.dart
Normal file
|
@ -0,0 +1,273 @@
|
|||
// 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,
|
||||
);
|
||||
}
|
41
packages/flutter_chat/lib/src/config/screen_types.dart
Normal file
41
packages/flutter_chat/lib/src/config/screen_types.dart
Normal file
|
@ -0,0 +1,41 @@
|
|||
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);
|
||||
}
|
218
packages/flutter_chat/lib/src/flutter_chat_entry_widget.dart
Normal file
218
packages/flutter_chat/lib/src/flutter_chat_entry_widget.dart
Normal file
|
@ -0,0 +1,218 @@
|
|||
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,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
// 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
323
packages/flutter_chat/lib/src/routes.dart
Normal file
323
packages/flutter_chat/lib/src/routes.dart
Normal file
|
@ -0,0 +1,323 @@
|
|||
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),
|
||||
);
|
|
@ -0,0 +1,583 @@
|
|||
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,
|
||||
],
|
||||
);
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -0,0 +1,504 @@
|
|||
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,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
}
|
320
packages/flutter_chat/lib/src/screens/chat_profile_screen.dart
Normal file
320
packages/flutter_chat/lib/src/screens/chat_profile_screen.dart
Normal file
|
@ -0,0 +1,320 @@
|
|||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
720
packages/flutter_chat/lib/src/screens/chat_screen.dart
Normal file
720
packages/flutter_chat/lib/src/screens/chat_screen.dart
Normal file
|
@ -0,0 +1,720 @@
|
|||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,454 @@
|
|||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,298 @@
|
|||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
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);
|
||||
}
|
||||
}
|
74
packages/flutter_chat/lib/src/services/date_formatter.dart
Normal file
74
packages/flutter_chat/lib/src/services/date_formatter.dart
Normal file
|
@ -0,0 +1,74 @@
|
|||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
24
packages/flutter_chat/lib/src/services/pop_handler.dart
Normal file
24
packages/flutter_chat/lib/src/services/pop_handler.dart
Normal file
|
@ -0,0 +1,24 @@
|
|||
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();
|
||||
}
|
||||
}
|
37
packages/flutter_chat/lib/src/util/scope.dart
Normal file
37
packages/flutter_chat/lib/src/util/scope.dart
Normal file
|
@ -0,0 +1,37 @@
|
|||
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>()!;
|
||||
}
|
18
packages/flutter_chat/lib/src/util/utils.dart
Normal file
18
packages/flutter_chat/lib/src/util/utils.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
// 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;
|
||||
}
|
37
packages/flutter_chat/pubspec.yaml
Normal file
37
packages/flutter_chat/pubspec.yaml
Normal file
|
@ -0,0 +1,37 @@
|
|||
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
|
14
packages/flutter_chat/test/relative_date_test.dart
Normal file
14
packages/flutter_chat/test/relative_date_test.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
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));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
# This is a generated file; do not edit or check into version control.
|
||||
flutter_plugin_android_lifecycle=/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-2.0.7/
|
||||
image_picker=/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/image_picker-0.8.6/
|
||||
image_picker_android=/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/image_picker_android-0.8.5+3/
|
||||
image_picker_for_web=/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/image_picker_for_web-2.1.10/
|
||||
image_picker_ios=/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/image_picker_ios-0.8.6+1/
|
|
@ -1 +0,0 @@
|
|||
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"image_picker_ios","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/image_picker_ios-0.8.6+1/","native_build":true,"dependencies":[]}],"android":[{"name":"flutter_plugin_android_lifecycle","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/flutter_plugin_android_lifecycle-2.0.7/","native_build":true,"dependencies":[]},{"name":"image_picker_android","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/image_picker_android-0.8.5+3/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"]}],"macos":[],"linux":[],"windows":[],"web":[{"name":"image_picker_for_web","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/image_picker_for_web-2.1.10/","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["image_picker_android","image_picker_for_web","image_picker_ios"]},{"name":"image_picker_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"image_picker_ios","dependencies":[]}],"date_created":"2022-10-18 09:32:26.979069","version":"3.3.4"}
|
|
@ -1,10 +0,0 @@
|
|||
# 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
|
|
@ -1,3 +0,0 @@
|
|||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
|
@ -1 +0,0 @@
|
|||
TODO: Add your license here.
|
|
@ -1,39 +0,0 @@
|
|||
<!--
|
||||
This README describes the package. If you publish this package to pub.dev,
|
||||
this README's contents appear on the landing page for your package.
|
||||
|
||||
For information about how to write a good package README, see the guide for
|
||||
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
|
||||
|
||||
For general information about developing packages, see the Dart guide for
|
||||
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
|
||||
and the Flutter guide for
|
||||
[developing packages and plugins](https://flutter.dev/developing-packages).
|
||||
-->
|
||||
|
||||
TODO: Put a short description of the package here that helps potential users
|
||||
know whether this package might be useful for them.
|
||||
|
||||
## Features
|
||||
|
||||
TODO: List what your package can do. Maybe include images, gifs, or videos.
|
||||
|
||||
## Getting started
|
||||
|
||||
TODO: List prerequisites and provide or point to information on how to
|
||||
start using the package.
|
||||
|
||||
## Usage
|
||||
|
||||
TODO: Include short and useful examples for package users. Add longer examples
|
||||
to `/example` folder.
|
||||
|
||||
```dart
|
||||
const like = 'sample';
|
||||
```
|
||||
|
||||
## Additional information
|
||||
|
||||
TODO: Tell users more about the package: where to find more information, how to
|
||||
contribute to the package, how to file issues, what response they can expect
|
||||
from the package authors, and more.
|
|
@ -1,4 +0,0 @@
|
|||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
|
@ -1,80 +0,0 @@
|
|||
library flutter_community_chat;
|
||||
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
|
||||
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
|
||||
import 'package:flutter_image_picker/flutter_image_picker.dart';
|
||||
export 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
|
||||
|
||||
class CommunityChat extends StatelessWidget {
|
||||
const CommunityChat({
|
||||
required this.dataProvider,
|
||||
this.chatOptions = const ChatOptions(),
|
||||
this.imagePickerTheme = const ImagePickerTheme(),
|
||||
super.key,
|
||||
});
|
||||
|
||||
final CommunityChatInterface dataProvider;
|
||||
final ChatOptions chatOptions;
|
||||
final ImagePickerTheme imagePickerTheme;
|
||||
|
||||
Future<void> _push(BuildContext context, Widget widget) =>
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => widget),
|
||||
);
|
||||
|
||||
Future<void> _onPressStartChat(BuildContext context) =>
|
||||
dataProvider.getChatUsers().then((users) => _push(
|
||||
context,
|
||||
NewChatScreen(
|
||||
chatOptions: chatOptions,
|
||||
onPressCreateChat: (user) => dataProvider.createChat(
|
||||
PersonalChatModel(user: user),
|
||||
),
|
||||
users: users,
|
||||
),
|
||||
));
|
||||
|
||||
Future<void> _onPressChat(BuildContext context, ChatModel chat) => _push(
|
||||
context,
|
||||
ChatDetailScreen(
|
||||
chatOptions: chatOptions,
|
||||
chat: chat,
|
||||
chatMessages: dataProvider.getMessagesStream(chat),
|
||||
onPressSelectImage: (ChatModel chat) =>
|
||||
_onPressSelectImage(context, chat),
|
||||
onMessageSubmit: (ChatModel chat, String content) =>
|
||||
dataProvider.sendTextMessage(chat, content),
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> _onPressSelectImage(BuildContext context, ChatModel chat) =>
|
||||
showModalBottomSheet<Uint8List?>(
|
||||
context: context,
|
||||
builder: (BuildContext context) =>
|
||||
chatOptions.imagePickerContainerBuilder(
|
||||
ImagePicker(
|
||||
customButton: chatOptions.closeImagePickerButtonBuilder(
|
||||
context,
|
||||
() => Navigator.of(context).pop(),
|
||||
),
|
||||
imagePickerTheme: imagePickerTheme,
|
||||
),
|
||||
),
|
||||
).then(
|
||||
(image) {
|
||||
if (image != null) {
|
||||
return dataProvider.sendImageMessage(chat, image);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChatScreen(
|
||||
chats: dataProvider.getChatsStream(),
|
||||
onPressStartChat: () => _onPressStartChat(context),
|
||||
onPressChat: (chat) => _onPressChat(context, chat),
|
||||
chatOptions: chatOptions,
|
||||
);
|
||||
}
|
|
@ -1,414 +0,0 @@
|
|||
# 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: "49.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.1.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.9.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
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.1"
|
||||
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"
|
||||
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_community_chat_interface:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "packages/flutter_community_chat_interface"
|
||||
ref: "0.0.3"
|
||||
resolved-ref: "3c88dcd3ce77f6703e9d2f3bb535f8117eb0b33d"
|
||||
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: "0.0.3"
|
||||
resolved-ref: "3c88dcd3ce77f6703e9d2f3bb535f8117eb0b33d"
|
||||
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: e348c921d5e621975e4f06d3eeff8e8a89dda109
|
||||
url: "https://github.com/Iconica-Development/flutter_data_interface.git"
|
||||
source: git
|
||||
version: "1.0.0"
|
||||
flutter_image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "v1.0.1"
|
||||
resolved-ref: "8a5ddd6ea1fcf85793777d9a3e6f10da2e2ac5f3"
|
||||
url: "https://github.com/Iconica-Development/flutter_image_picker"
|
||||
source: git
|
||||
version: "1.0.0"
|
||||
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.0"
|
||||
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.1"
|
||||
image_picker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.8.6"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.8.5+3"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.10"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.8.6+1"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.6.2"
|
||||
intl:
|
||||
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.0"
|
||||
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.12"
|
||||
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"
|
||||
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.2"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
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"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.10.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.1.1"
|
||||
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.14"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
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.1"
|
||||
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: ">=2.10.0"
|
|
@ -1,67 +0,0 @@
|
|||
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
|
||||
ref: 0.0.3
|
||||
flutter_community_chat_interface:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_community_chat.git
|
||||
path: packages/flutter_community_chat_interface
|
||||
ref: 0.0.3
|
||||
flutter_image_picker:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_image_picker
|
||||
ref: v1.0.1
|
||||
|
||||
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
|
|
@ -1,9 +0,0 @@
|
|||
# This is a generated file; do not edit or check into version control.
|
||||
cloud_firestore=/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/cloud_firestore-3.2.0/
|
||||
cloud_firestore_web=/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/cloud_firestore_web-2.8.10/
|
||||
firebase_auth=/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_auth-3.11.2/
|
||||
firebase_auth_web=/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_auth_web-4.6.1/
|
||||
firebase_core=/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_core-1.24.0/
|
||||
firebase_core_web=/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_core_web-1.7.3/
|
||||
firebase_storage=/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_storage-10.3.11/
|
||||
firebase_storage_web=/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_storage_web-3.3.9/
|
|
@ -1 +0,0 @@
|
|||
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"cloud_firestore","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/cloud_firestore-3.2.0/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_auth","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_auth-3.11.2/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_core","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_core-1.24.0/","native_build":true,"dependencies":[]},{"name":"firebase_storage","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_storage-10.3.11/","native_build":true,"dependencies":["firebase_core"]}],"android":[{"name":"cloud_firestore","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/cloud_firestore-3.2.0/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_auth","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_auth-3.11.2/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_core","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_core-1.24.0/","native_build":true,"dependencies":[]},{"name":"firebase_storage","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_storage-10.3.11/","native_build":true,"dependencies":["firebase_core"]}],"macos":[{"name":"cloud_firestore","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/cloud_firestore-3.2.0/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_auth","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_auth-3.11.2/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_core","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_core-1.24.0/","native_build":true,"dependencies":[]},{"name":"firebase_storage","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_storage-10.3.11/","native_build":true,"dependencies":["firebase_core"]}],"linux":[],"windows":[],"web":[{"name":"cloud_firestore_web","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/cloud_firestore_web-2.8.10/","dependencies":["firebase_core_web"]},{"name":"firebase_auth_web","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_auth_web-4.6.1/","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_core_web-1.7.3/","dependencies":[]},{"name":"firebase_storage_web","path":"/Users/steinmilder/.pub-cache/hosted/pub.dartlang.org/firebase_storage_web-3.3.9/","dependencies":["firebase_core_web"]}]},"dependencyGraph":[{"name":"cloud_firestore","dependencies":["cloud_firestore_web","firebase_core"]},{"name":"cloud_firestore_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"firebase_auth","dependencies":["firebase_auth_web","firebase_core"]},{"name":"firebase_auth_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"firebase_core","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","dependencies":[]},{"name":"firebase_storage","dependencies":["firebase_core","firebase_storage_web"]},{"name":"firebase_storage_web","dependencies":["firebase_core","firebase_core_web"]}],"date_created":"2022-10-18 09:33:20.309805","version":"3.3.4"}
|
|
@ -1,10 +0,0 @@
|
|||
# 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
|
|
@ -1,3 +0,0 @@
|
|||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
|
@ -1 +0,0 @@
|
|||
TODO: Add your license here.
|
|
@ -1,39 +0,0 @@
|
|||
<!--
|
||||
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.
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue