From f28cc91301748f18e10c497dedf2d8cb3950d351 Mon Sep 17 00:00:00 2001 From: Neevash Ramdial Date: Thu, 7 Jan 2021 12:09:26 -0400 Subject: [PATCH] add dart client to packages --- packages/dart_client/.gitignore | 60 + packages/dart_client/.metadata | 10 + packages/dart_client/CHANGELOG.md | 424 ++ packages/dart_client/LICENSE | 219 + packages/dart_client/README.md | 107 + packages/dart_client/analysis_options.yaml | 62 + packages/dart_client/build.yaml | 11 + packages/dart_client/example/README.md | 1 + .../dart_client/example/android/.gitignore | 7 + .../example/android/app/build.gradle | 67 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 31 + .../example/stream_chat_dart/MainActivity.kt | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values/styles.xml | 8 + .../app/src/profile/AndroidManifest.xml | 7 + .../dart_client/example/android/build.gradle | 31 + .../example/android/gradle.properties | 4 + .../gradle/wrapper/gradle-wrapper.properties | 6 + .../example/android/settings.gradle | 15 + packages/dart_client/example/ios/.gitignore | 32 + .../ios/Flutter/AppFrameworkInfo.plist | 26 + .../example/ios/Flutter/Debug.xcconfig | 2 + .../example/ios/Flutter/Release.xcconfig | 2 + .../ios/Runner.xcodeproj/project.pbxproj | 584 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/xcschemes/Runner.xcscheme | 91 + .../contents.xcworkspacedata | 10 + .../example/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 + .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 564 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 1588 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 1025 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 1716 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 1920 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 1895 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 3831 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 1888 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 3294 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 3612 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + .../dart_client/example/ios/Runner/Info.plist | 50 + .../ios/Runner/Runner-Bridging-Header.h | 1 + .../example/lib/channel_header.dart | 83 + .../example/lib/channel_image.dart | 25 + .../example/lib/channel_list_page.dart | 138 + .../example/lib/channel_list_view.dart | 208 + .../example/lib/channel_name_text.dart | 19 + .../example/lib/channel_page_app_bar.dart | 41 + .../example/lib/channel_preview.dart | 139 + .../example/lib/connection_indicator.dart | 100 + packages/dart_client/example/lib/main.dart | 94 + .../example/lib/message_input.dart | 137 + .../example/lib/message_list_view.dart | 280 ++ .../dart_client/example/lib/message_page.dart | 110 + .../example/lib/message_widget.dart | 353 ++ .../example/lib/stream_channel.dart | 128 + .../dart_client/example/lib/stream_chat.dart | 95 + .../dart_client/example/lib/user_avatar.dart | 29 + packages/dart_client/example/pubspec.yaml | 33 + .../dart_client/example/test/widget_test.dart | 29 + .../example/web/icons/Icon-192.png | Bin 0 -> 5292 bytes .../example/web/icons/Icon-512.png | Bin 0 -> 8252 bytes packages/dart_client/example/web/index.html | 20 + .../dart_client/example/web/manifest.json | 23 + packages/dart_client/lib/src/api/channel.dart | 1518 ++++++ .../lib/src/api/connection_status.dart | 11 + .../dart_client/lib/src/api/requests.dart | 97 + .../dart_client/lib/src/api/requests.g.dart | 31 + .../dart_client/lib/src/api/responses.dart | 375 ++ .../dart_client/lib/src/api/responses.g.dart | 382 ++ .../dart_client/lib/src/api/retry_policy.dart | 37 + .../dart_client/lib/src/api/retry_queue.dart | 202 + .../lib/src/api/web_socket_channel_html.dart | 7 + .../lib/src/api/web_socket_channel_io.dart | 7 + .../lib/src/api/web_socket_channel_stub.dart | 9 + .../dart_client/lib/src/api/websocket.dart | 295 ++ packages/dart_client/lib/src/client.dart | 1388 ++++++ .../dart_client/lib/src/db/models.part.dart | 258 + .../lib/src/db/offline_storage.dart | 841 ++++ .../lib/src/db/offline_storage.g.dart | 4160 ++++++++++++++++ .../lib/src/db/shared/native_db.dart | 64 + .../lib/src/db/shared/shared_db.dart | 3 + .../lib/src/db/shared/unsupported_db.dart | 12 + .../dart_client/lib/src/db/shared/web_db.dart | 18 + packages/dart_client/lib/src/event_type.dart | 94 + packages/dart_client/lib/src/exceptions.dart | 54 + .../dart_client/lib/src/models/action.dart | 31 + .../dart_client/lib/src/models/action.g.dart | 25 + .../lib/src/models/attachment.dart | 207 + .../lib/src/models/attachment.g.dart | 74 + .../lib/src/models/channel_config.dart | 84 + .../lib/src/models/channel_config.g.dart | 58 + .../lib/src/models/channel_model.dart | 145 + .../lib/src/models/channel_model.g.dart | 69 + .../lib/src/models/channel_state.dart | 66 + .../lib/src/models/channel_state.g.dart | 56 + .../dart_client/lib/src/models/command.dart | 30 + .../dart_client/lib/src/models/command.g.dart | 21 + .../dart_client/lib/src/models/device.dart | 25 + .../dart_client/lib/src/models/device.g.dart | 19 + .../dart_client/lib/src/models/event.dart | 183 + .../dart_client/lib/src/models/event.g.dart | 156 + .../dart_client/lib/src/models/member.dart | 96 + .../dart_client/lib/src/models/member.g.dart | 49 + .../dart_client/lib/src/models/message.dart | 278 ++ .../dart_client/lib/src/models/message.g.dart | 135 + packages/dart_client/lib/src/models/mute.dart | 36 + .../dart_client/lib/src/models/mute.g.dart | 44 + .../dart_client/lib/src/models/own_user.dart | 83 + .../lib/src/models/own_user.g.dart | 77 + .../dart_client/lib/src/models/reaction.dart | 68 + .../lib/src/models/reaction.g.dart | 47 + packages/dart_client/lib/src/models/read.dart | 31 + .../dart_client/lib/src/models/read.g.dart | 27 + .../lib/src/models/serialization.dart | 49 + packages/dart_client/lib/src/models/user.dart | 108 + .../dart_client/lib/src/models/user.g.dart | 51 + .../dart_client/lib/src/notifications.dart | 119 + packages/dart_client/lib/stream_chat.dart | 29 + packages/dart_client/lib/version.dart | 5 + packages/dart_client/peanut.yaml | 3 + packages/dart_client/pubspec.yaml | 36 + .../test/src/api/channel_test.dart | 2099 ++++++++ .../test/src/api/requests_test.dart | 18 + .../test/src/api/responses_test.dart | 4336 +++++++++++++++++ .../test/src/api/web_socket_stub_test.dart | 9 + .../test/src/api/websocket_test.dart | 353 ++ .../dart_client/test/src/client_test.dart | 979 ++++ .../test/src/models/action_test.dart | 46 + .../test/src/models/attachment_test.dart | 69 + .../test/src/models/channel_state_test.dart | 1112 +++++ .../test/src/models/channel_test.dart | 56 + .../test/src/models/command_test.dart | 40 + .../test/src/models/device_test.dart | 31 + .../test/src/models/event_test.dart | 89 + .../test/src/models/member_test.dart | 35 + .../test/src/models/message_test.dart | 153 + .../test/src/models/reaction_test.dart | 72 + .../test/src/models/read_test.dart | 40 + .../test/src/models/serialization_test.dart | 60 + .../test/src/models/user_test.dart | 28 + packages/dart_client/test/version_test.dart | 23 + .../test_integration/client_test.dart | 21 + 160 files changed, 26368 insertions(+) create mode 100644 packages/dart_client/.gitignore create mode 100644 packages/dart_client/.metadata create mode 100644 packages/dart_client/CHANGELOG.md create mode 100644 packages/dart_client/LICENSE create mode 100644 packages/dart_client/README.md create mode 100644 packages/dart_client/analysis_options.yaml create mode 100644 packages/dart_client/build.yaml create mode 100644 packages/dart_client/example/README.md create mode 100644 packages/dart_client/example/android/.gitignore create mode 100644 packages/dart_client/example/android/app/build.gradle create mode 100644 packages/dart_client/example/android/app/src/debug/AndroidManifest.xml create mode 100644 packages/dart_client/example/android/app/src/main/AndroidManifest.xml create mode 100644 packages/dart_client/example/android/app/src/main/kotlin/com/example/stream_chat_dart/MainActivity.kt create mode 100644 packages/dart_client/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 packages/dart_client/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 packages/dart_client/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 packages/dart_client/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 packages/dart_client/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 packages/dart_client/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 packages/dart_client/example/android/app/src/main/res/values/styles.xml create mode 100644 packages/dart_client/example/android/app/src/profile/AndroidManifest.xml create mode 100644 packages/dart_client/example/android/build.gradle create mode 100644 packages/dart_client/example/android/gradle.properties create mode 100644 packages/dart_client/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/dart_client/example/android/settings.gradle create mode 100644 packages/dart_client/example/ios/.gitignore create mode 100644 packages/dart_client/example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 packages/dart_client/example/ios/Flutter/Debug.xcconfig create mode 100644 packages/dart_client/example/ios/Flutter/Release.xcconfig create mode 100644 packages/dart_client/example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 packages/dart_client/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 packages/dart_client/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 packages/dart_client/example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packages/dart_client/example/ios/Runner/AppDelegate.swift create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 packages/dart_client/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 packages/dart_client/example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 packages/dart_client/example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 packages/dart_client/example/ios/Runner/Info.plist create mode 100644 packages/dart_client/example/ios/Runner/Runner-Bridging-Header.h create mode 100644 packages/dart_client/example/lib/channel_header.dart create mode 100644 packages/dart_client/example/lib/channel_image.dart create mode 100644 packages/dart_client/example/lib/channel_list_page.dart create mode 100644 packages/dart_client/example/lib/channel_list_view.dart create mode 100644 packages/dart_client/example/lib/channel_name_text.dart create mode 100644 packages/dart_client/example/lib/channel_page_app_bar.dart create mode 100644 packages/dart_client/example/lib/channel_preview.dart create mode 100644 packages/dart_client/example/lib/connection_indicator.dart create mode 100644 packages/dart_client/example/lib/main.dart create mode 100644 packages/dart_client/example/lib/message_input.dart create mode 100644 packages/dart_client/example/lib/message_list_view.dart create mode 100644 packages/dart_client/example/lib/message_page.dart create mode 100644 packages/dart_client/example/lib/message_widget.dart create mode 100644 packages/dart_client/example/lib/stream_channel.dart create mode 100644 packages/dart_client/example/lib/stream_chat.dart create mode 100644 packages/dart_client/example/lib/user_avatar.dart create mode 100644 packages/dart_client/example/pubspec.yaml create mode 100644 packages/dart_client/example/test/widget_test.dart create mode 100644 packages/dart_client/example/web/icons/Icon-192.png create mode 100644 packages/dart_client/example/web/icons/Icon-512.png create mode 100644 packages/dart_client/example/web/index.html create mode 100644 packages/dart_client/example/web/manifest.json create mode 100644 packages/dart_client/lib/src/api/channel.dart create mode 100644 packages/dart_client/lib/src/api/connection_status.dart create mode 100644 packages/dart_client/lib/src/api/requests.dart create mode 100644 packages/dart_client/lib/src/api/requests.g.dart create mode 100644 packages/dart_client/lib/src/api/responses.dart create mode 100644 packages/dart_client/lib/src/api/responses.g.dart create mode 100644 packages/dart_client/lib/src/api/retry_policy.dart create mode 100644 packages/dart_client/lib/src/api/retry_queue.dart create mode 100644 packages/dart_client/lib/src/api/web_socket_channel_html.dart create mode 100644 packages/dart_client/lib/src/api/web_socket_channel_io.dart create mode 100644 packages/dart_client/lib/src/api/web_socket_channel_stub.dart create mode 100644 packages/dart_client/lib/src/api/websocket.dart create mode 100644 packages/dart_client/lib/src/client.dart create mode 100644 packages/dart_client/lib/src/db/models.part.dart create mode 100644 packages/dart_client/lib/src/db/offline_storage.dart create mode 100644 packages/dart_client/lib/src/db/offline_storage.g.dart create mode 100644 packages/dart_client/lib/src/db/shared/native_db.dart create mode 100644 packages/dart_client/lib/src/db/shared/shared_db.dart create mode 100644 packages/dart_client/lib/src/db/shared/unsupported_db.dart create mode 100644 packages/dart_client/lib/src/db/shared/web_db.dart create mode 100644 packages/dart_client/lib/src/event_type.dart create mode 100644 packages/dart_client/lib/src/exceptions.dart create mode 100644 packages/dart_client/lib/src/models/action.dart create mode 100644 packages/dart_client/lib/src/models/action.g.dart create mode 100644 packages/dart_client/lib/src/models/attachment.dart create mode 100644 packages/dart_client/lib/src/models/attachment.g.dart create mode 100644 packages/dart_client/lib/src/models/channel_config.dart create mode 100644 packages/dart_client/lib/src/models/channel_config.g.dart create mode 100644 packages/dart_client/lib/src/models/channel_model.dart create mode 100644 packages/dart_client/lib/src/models/channel_model.g.dart create mode 100644 packages/dart_client/lib/src/models/channel_state.dart create mode 100644 packages/dart_client/lib/src/models/channel_state.g.dart create mode 100644 packages/dart_client/lib/src/models/command.dart create mode 100644 packages/dart_client/lib/src/models/command.g.dart create mode 100644 packages/dart_client/lib/src/models/device.dart create mode 100644 packages/dart_client/lib/src/models/device.g.dart create mode 100644 packages/dart_client/lib/src/models/event.dart create mode 100644 packages/dart_client/lib/src/models/event.g.dart create mode 100644 packages/dart_client/lib/src/models/member.dart create mode 100644 packages/dart_client/lib/src/models/member.g.dart create mode 100644 packages/dart_client/lib/src/models/message.dart create mode 100644 packages/dart_client/lib/src/models/message.g.dart create mode 100644 packages/dart_client/lib/src/models/mute.dart create mode 100644 packages/dart_client/lib/src/models/mute.g.dart create mode 100644 packages/dart_client/lib/src/models/own_user.dart create mode 100644 packages/dart_client/lib/src/models/own_user.g.dart create mode 100644 packages/dart_client/lib/src/models/reaction.dart create mode 100644 packages/dart_client/lib/src/models/reaction.g.dart create mode 100644 packages/dart_client/lib/src/models/read.dart create mode 100644 packages/dart_client/lib/src/models/read.g.dart create mode 100644 packages/dart_client/lib/src/models/serialization.dart create mode 100644 packages/dart_client/lib/src/models/user.dart create mode 100644 packages/dart_client/lib/src/models/user.g.dart create mode 100644 packages/dart_client/lib/src/notifications.dart create mode 100644 packages/dart_client/lib/stream_chat.dart create mode 100644 packages/dart_client/lib/version.dart create mode 100644 packages/dart_client/peanut.yaml create mode 100644 packages/dart_client/pubspec.yaml create mode 100644 packages/dart_client/test/src/api/channel_test.dart create mode 100644 packages/dart_client/test/src/api/requests_test.dart create mode 100644 packages/dart_client/test/src/api/responses_test.dart create mode 100644 packages/dart_client/test/src/api/web_socket_stub_test.dart create mode 100644 packages/dart_client/test/src/api/websocket_test.dart create mode 100644 packages/dart_client/test/src/client_test.dart create mode 100644 packages/dart_client/test/src/models/action_test.dart create mode 100644 packages/dart_client/test/src/models/attachment_test.dart create mode 100644 packages/dart_client/test/src/models/channel_state_test.dart create mode 100644 packages/dart_client/test/src/models/channel_test.dart create mode 100644 packages/dart_client/test/src/models/command_test.dart create mode 100644 packages/dart_client/test/src/models/device_test.dart create mode 100644 packages/dart_client/test/src/models/event_test.dart create mode 100644 packages/dart_client/test/src/models/member_test.dart create mode 100644 packages/dart_client/test/src/models/message_test.dart create mode 100644 packages/dart_client/test/src/models/reaction_test.dart create mode 100644 packages/dart_client/test/src/models/read_test.dart create mode 100644 packages/dart_client/test/src/models/serialization_test.dart create mode 100644 packages/dart_client/test/src/models/user_test.dart create mode 100644 packages/dart_client/test/version_test.dart create mode 100644 packages/dart_client/test_integration/client_test.dart diff --git a/packages/dart_client/.gitignore b/packages/dart_client/.gitignore new file mode 100644 index 00000000..2ea557c4 --- /dev/null +++ b/packages/dart_client/.gitignore @@ -0,0 +1,60 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# 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 +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ +coverage/ +coverage_helper_test.dart + +# Web related +lib/generated_plugin_registrant.dart + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +build/ +# If you're building an application, you may want to check-in your pubspec.lock +pubspec.lock + +# Directory created by dartdoc +# If you don't generate documentation locally you can remove this line. +doc/api/ + +# Avoid committing generated Javascript files: +*.dart.js +*.info.json # Produced by the --dump-info flag. +*.js # When generated by dart2js. Don't specify *.js if your + # project includes source files written in JavaScript. +*.js_ +*.js.deps +*.js.map diff --git a/packages/dart_client/.metadata b/packages/dart_client/.metadata new file mode 100644 index 00000000..946cda16 --- /dev/null +++ b/packages/dart_client/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 659dc8129d4edb9166e9a0d600439d135740933f + channel: beta + +project_type: package diff --git a/packages/dart_client/CHANGELOG.md b/packages/dart_client/CHANGELOG.md new file mode 100644 index 00000000..a570a254 --- /dev/null +++ b/packages/dart_client/CHANGELOG.md @@ -0,0 +1,424 @@ +## 0.2.23+1 + +- Throw an error when calling queryChannels without an active connection +- Wait to establish a connection if calling queryChannels while connecting + +## 0.2.23 + +- Add thread_participants in message model + +## 0.2.22 + +- Add thread-less message reply feature (QuotedMessage) + +## 0.2.21+2 + +- Fix but not throwing error during querychannels and persistance disabled +- Fix reaction.updated event handling + +## 0.2.21+1 + +- Fix error in the offline storage queryChannelCids query + +## 0.2.21 + +- Fix channel.hide(clearHistory: true) not clearing local messages +- Add banned field to member + +## 0.2.20 + +- Return offline data only if the backend is unreachable. This avoids the glitch of the ChannelListView because we cannot sort by custom properties. + +## 0.2.19 + +- Added message filters for `Client.search()` + +## 0.2.18 + +- Correctly dispose resources when disposing the client state +- Limit parallel queryChannels with same parameters to 1 +- Added `clearUser` parameter to `client.disconnect` to remove the user instance of the client + +## 0.2.17+1 + +- Do not retry messages when server returns error + +## 0.2.17 + +- Add shadow ban feature + +## 0.2.16 + +- Listen for user.updated events + +## 0.2.15+2 + +- Fix reaction score updates + +## 0.2.15+1 + +- Listen to reaction.updated event + +## 0.2.15 + +- Fix search message response + +## 0.2.14 + +- Add event.extradata + +## 0.2.13+1 + +- Let user change channel.extradata if the channel is not initialized yet + +## 0.2.13 + +- Add parent_id to events for typing indicators in threads + +## 0.2.12+2 + +- Fix error with reactions with null user + +## 0.2.12 + +- Do not save channels in memory if not being watched. This was leading to some bugs in some specific use-cases. + +## 0.2.11 + +- Fix user.name getter +- Use detached loggers +- Throw error while connecting if it comes from backend +- Fix ws reconnection + +## 0.2.10+2 + +- Fix bug with event filtering + +## 0.2.10+1 + +- Add default limit to pagination + +## 0.2.10 + +- Added `channel.state.unreadCountStream` + +## 0.2.9 + +- Adding a message on `Channel.update` is now optional + +## 0.2.8+1 + +- Fix retry logic + +## 0.2.8 + +- Add missing event types +- Fix local sorting on offline storage + +## 0.2.7+1 + +- `Client.channel` returns an existing channel if available +- Update message in the offline storage if attachment has expired (for the new CDN) +- Fix `GetMessagesByIdResponse` format +- Do not query messages if already existing in offline storage + +## 0.2.6 + +- Experimental support for Flutter web and MacOs + +## 0.2.5+2 + +- Cleaned up Serialization on extra_data + +## 0.2.5+1 + +- Fix `channel.show` api call + +## 0.2.5 + +- Add `channelType` and `channelId` properties to event object + +## 0.2.4+2 + +- Fix query members messing channel state + +## 0.2.4+1 + +- Do not resync if there is no channel in offlinestorage + +## 0.2.4 + +- Add null-safety to ws disconnect +- Add pagination parameters to queryUsers request + +## 0.2.3+3 + +- Fix reaction add/remove logic + +## 0.2.3+2 + +- Skip system messages during unreadCount computation + +## 0.2.3+1 + +- Removed moor_ffi from dependencies in favor of moor/ffi + +## 0.2.3 + +- Fix reject invite payload + +- Add multi-tenant properties to channel and user + +## 0.2.2+1 + +- Fix queryChannels payload + +## 0.2.2 + +- Fix add/remove/invite members api calls + +## 0.2.1 + +- Add `isMutedStream` to `Channel` +- Add `isGroup` to `Channel` +- Add `isDistinct` to `Channel` + +## 0.2.0+2 + +- Fix search messages response class + +## 0.2.0+1 + +- Fix offline members update +- Add channel mutes +- Fix default channel sort + +## 0.2.0 + +- Add `lastMessage` getter to Channel.state +- Add `isSystem` property to Message +- Incremental websocket reconnection timeout +- Add translate message api call +- Add queryMembers api call +- Add user list to client state +- Synchronize channel members status +- Add offline storage +- Add push notifications helper functions + +## 0.2.0-alpha+23 + +- Add `lastMessage` getter to `Channel.state` + +## 0.2.0-alpha+22 + +- Add `isSystem` property to Message + +## 0.2.0-alpha+21 + +- Incremental websocket reconnection timeout + +## 0.2.0-alpha+20 + +- More robust offline storage insertions + +## 0.2.0-alpha+19 + +- Add translate message api call +- Add queryMembers api call + +## 0.2.0-alpha+18 + +- Revert moor_ffi version to 0.5.0 + +## 0.2.0-alpha+17 + +- Add user list to client + +- Synchronize channel members status + +## 0.2.0-alpha+16 + +- Try QueryChannels when `resync` endpoint returns an error + +## 0.2.0-alpha+15 + +- Fix receiving reactions + +## 0.2.0-alpha+14 + +- Avoid sending local event for optimistic updates + +## 0.2.0-alpha+13 + +- Fix offline on app first start up + +## 0.2.0-alpha+12 + +- Fix retry mechanism in threads +- Fix delete channel query + +## 0.2.0-alpha+9 + +- Add retry mechanism and retry queue + +## 0.2.0-alpha+8 + +- Add copyWith to Attachment + +## 0.2.0-alpha+7 + +- Add channel deleted/updated event handling + +## 0.2.0-alpha+6 + +- Align with stable release + +## 0.2.0-alpha+5 + +- Rename client parameters + +## 0.2.0-alpha+3 + +- Remove dependencies on notification service + +- Expose some helping method for integrate offline storage with push notifications + +## 0.2.0-alpha+2 + +- Fix unread count + +## 0.2.0-alpha + +- Offline storage + +- Push notifications + +- Minor bug fixes + +## 0.1.30 + +- Add silent property to message + +## 0.1.29 + +- Fix read event handling + +## 0.1.28 + +- Fix bug clearing members when receiving a message + +## 0.1.27 + +- Update dependencies + +## 0.1.26 + +- Remove wrong `members` property from `ChannelModel` + +## 0.1.25 + +- Fix online status + +## 0.1.24 + +- Fix unread count + +## 0.1.22 + +- Add mute/unmute channel + +## 0.1.20 + +- Fix channel query path without id + +## 0.1.19 + +- Fix loading message replies + +## 0.1.18 + +- Export dio error + +## 0.1.17 + +- Ignore current user typing events + +- Add event types + +## 0.1.16 + +- Fix message update + +## 0.1.15 + +- Fix mentions handling + +## 0.1.14 + +- Handle message modification and commands + +## 0.1.13 + +- Add message.updated event handling + +## 0.1.12 + +- Add export multipart_file from dio + +## 0.1.11 + +- Add channel config checks + +## 0.1.10 + +- Rename Channel.channelClients to channels + +## 0.1.9 + +- Fix channel update on message delete + +## 0.1.8 + +- Add delete message handling + +## 0.1.7 + +- Add reaction handling + +## 0.1.6 + +- Add initialized completer + +- Update example + +## 0.1.5 + +- Add `ClientState` and `ChannelClientState` classes to handle channel state updates using events + +- Update example supporting threads + +## 0.1.4 + +- Update some api with wrong or incomplete signatures + +- Add documentation for public apis + +## 0.1.2 + +- add websocket reconnection logic + +- add token expiration mechanism + +## 0.1.1 + +- add typing events handling + +## 0.1.0 + +- a better example can be found in the example/ directory + +- fix some api calls and add missing one + +## 0.0.2 + +- first beta version \ No newline at end of file diff --git a/packages/dart_client/LICENSE b/packages/dart_client/LICENSE new file mode 100644 index 00000000..f2d1eaf3 --- /dev/null +++ b/packages/dart_client/LICENSE @@ -0,0 +1,219 @@ +SOURCE CODE LICENSE AGREEMENT + +IMPORTANT - READ THIS CAREFULLY BEFORE DOWNLOADING, INSTALLING, USING OR +ELECTRONICALLY ACCESSING THIS PROPRIETARY PRODUCT. + +THIS IS A LEGAL AGREEMENT BETWEEN STREAM.IO, INC. (“STREAM.IO”) AND THE +BUSINESS ENTITY OR PERSON FOR WHOM YOU (“YOU”) ARE ACTING (“CUSTOMER”) AS THE +LICENSEE OF THE PROPRIETARY SOFTWARE INTO WHICH THIS AGREEMENT HAS BEEN +INCLUDED (THE “AGREEMENT”). YOU AGREE THAT YOU ARE THE CUSTOMER, OR YOU ARE AN +EMPLOYEE OR AGENT OF CUSTOMER AND ARE ENTERING INTO THIS AGREEMENT FOR LICENSE +OF THE SOFTWARE BY CUSTOMER FOR CUSTOMER’S BUSINESS PURPOSES AS DESCRIBED IN +AND IN ACCORDANCE WITH THIS AGREEMENT. YOU HEREBY AGREE THAT YOU ENTER INTO +THIS AGREEMENT ON BEHALF OF CUSTOMER AND THAT YOU HAVE THE AUTHORITY TO BIND +CUSTOMER TO THIS AGREEMENT. + +STREAM.IO IS WILLING TO LICENSE THE SOFTWARE TO CUSTOMER ONLY ON THE FOLLOWING +CONDITIONS: (1) YOU ARE A CURRENT CUSTOMER OF STREAM.IO; (2) YOU ARE NOT A +COMPETITOR OF STREAM.IO; AND (3) THAT YOU ACCEPT ALL THE TERMS IN THIS +AGREEMENT. BY DOWNLOADING, INSTALLING, CONFIGURING, ACCESSING OR OTHERWISE +USING THE SOFTWARE, INCLUDING ANY UPDATES, UPGRADES, OR NEWER VERSIONS, YOU +REPRESENT, WARRANT AND ACKNOWLEDGE THAT (A) CUSTOMER IS A CURRENT CUSTOMER OF +STREAM.IO; (B) CUSTOMER IS NOT A COMPETITOR OF STREAM.IO; AND THAT (C) YOU HAVE +READ THIS AGREEMENT, UNDERSTAND THIS AGREEMENT, AND THAT CUSTOMER AGREES TO BE +BOUND BY ALL THE TERMS OF THIS AGREEMENT. + +IF YOU DO NOT AGREE TO ALL THE TERMS AND CONDITIONS OF THIS AGREEMENT, +STREAM.IO IS UNWILLING TO LICENSE THE SOFTWARE TO CUSTOMER, AND THEREFORE, DO +NOT COMPLETE THE DOWNLOAD PROCESS, ACCESS OR OTHERWISE USE THE SOFTWARE, AND +CUSTOMER SHOULD IMMEDIATELY RETURN THE SOFTWARE AND CEASE ANY USE OF THE +SOFTWARE. + +1. SOFTWARE. The Stream.io software accompanying this Agreement, may include +Source Code, Executable Object Code, associated media, printed materials and +documentation (collectively, the “Software”). The Software also includes any +updates or upgrades to or new versions of the original Software, if and when +made available to you by Stream.io. “Source Code” means computer programming +code in human readable form that is not suitable for machine execution without +the intervening steps of interpretation or compilation. “Executable Object +Code" means the computer programming code in any other form than Source Code +that is not readily perceivable by humans and suitable for machine execution +without the intervening steps of interpretation or compilation. “Site” means a +Customer location controlled by Customer. “Authorized User” means any employee +or contractor of Customer working at the Site, who has signed a written +confidentiality agreement with Customer or is otherwise bound in writing by +confidentiality and use obligations at least as restrictive as those imposed +under this Agreement. + +2. LICENSE GRANT. Subject to the terms and conditions of this Agreement, in +consideration for the representations, warranties, and covenants made by +Customer in this Agreement, Stream.io grants to Customer, during the term of +this Agreement, a personal, non-exclusive, non-transferable, non-sublicensable +license to: + +a. install and use Software Source Code on password protected computers at a Site, +restricted to Authorized Users; + +b. create derivative works, improvements (whether or not patentable), extensions +and other modifications to the Software Source Code (“Modifications”) to build +unique scalable newsfeeds, activity streams, and in-app messaging via Stream’s +application program interface (“API”); + +c. compile the Software Source Code to create Executable Object Code versions of +the Software Source Code and Modifications to build such newsfeeds, activity +streams, and in-app messaging via the API; + +d. install, execute and use such Executable Object Code versions solely for +Customer’s internal business use (including development of websites through +which data generated by Stream services will be streamed (“Apps”)); + +e. use and distribute such Executable Object Code as part of Customer’s Apps; and + +f. make electronic copies of the Software and Modifications as required for backup +or archival purposes. + +3. RESTRICTIONS. Customer is responsible for all activities that occur in +connection with the Software. Customer will not, and will not attempt to: (a) +sublicense or transfer the Software or any Source Code related to the Software +or any of Customer’s rights under this Agreement, except as otherwise provided +in this Agreement, (b) use the Software Source Code for the benefit of a third +party or to operate a service; (c) allow any third party to access or use the +Software Source Code; (d) sublicense or distribute the Software Source Code or +any Modifications in Source Code or other derivative works based on any part of +the Software Source Code; (e) use the Software in any manner that competes with +Stream.io or its business; or (e) otherwise use the Software in any manner that +exceeds the scope of use permitted in this Agreement. Customer shall use the +Software in compliance with any accompanying documentation any laws applicable +to Customer. + +4. OPEN SOURCE. Customer and its Authorized Users shall not use any software or +software components that are open source in conjunction with the Software +Source Code or any Modifications in Source Code or in any way that could +subject the Software to any open source licenses. + +5. CONTRACTORS. Under the rights granted to Customer under this Agreement, +Customer may permit its employees, contractors, and agencies of Customer to +become Authorized Users to exercise the rights to the Software granted to +Customer in accordance with this Agreement solely on behalf of Customer to +provide services to Customer; provided that Customer shall be liable for the +acts and omissions of all Authorized Users to the extent any of such acts or +omissions, if performed by Customer, would constitute a breach of, or otherwise +give rise to liability to Customer under, this Agreement. Customer shall not +and shall not permit any Authorized User to use the Software except as +expressly permitted in this Agreement. + +6. COMPETITIVE PRODUCT DEVELOPMENT. Customer shall not use the Software in any way +to engage in the development of products or services which could be reasonably +construed to provide a complete or partial functional or commercial alternative +to Stream.io’s products or services (a “Competitive Product”). Customer shall +ensure that there is no direct or indirect use of, or sharing of, Software +source code, or other information based upon or derived from the Software to +develop such products or services. Without derogating from the generality of +the foregoing, development of Competitive Products shall include having direct +or indirect access to, supervising, consulting or assisting in the development +of, or producing any specifications, documentation, object code or source code +for, all or part of a Competitive Product. + +7. LIMITATION ON MODIFICATIONS. Notwithstanding any provision in this Agreement, +Modifications may only be created and used by Customer as permitted by this +Agreement and Modification Source Code may not be distributed to third parties. +Customer will not assert against Stream.io, its affiliates, or their customers, +direct or indirect, agents and contractors, in any way, any patent rights that +Customer may obtain relating to any Modifications for Stream.io, its +affiliates’, or their customers’, direct or indirect, agents’ and contractors’ +manufacture, use, import, offer for sale or sale of any Stream.io products or +services. + +8. DELIVERY AND ACCEPTANCE. The Software will be delivered electronically pursuant +to Stream.io standard download procedures. The Software is deemed accepted upon +delivery. + +9. IMPLEMENTATION AND SUPPORT. Stream.io has no obligation under this Agreement to +provide any support or consultation concerning the Software. + +10. TERM AND TERMINATION. The term of this Agreement begins when the Software is +downloaded or accessed and shall continue until terminated. Either party may +terminate this Agreement upon written notice. This Agreement shall +automatically terminate if Customer is or becomes a competitor of Stream.io or +makes or sells any Competitive Products. Upon termination of this Agreement for +any reason, (a) all rights granted to Customer in this Agreement immediately +cease to exist, (b) Customer must promptly discontinue all use of the Software +and return to Stream.io or destroy all copies of the Software in Customer’s +possession or control. Any continued use of the Software by Customer or attempt +by Customer to exercise any rights under this Agreement after this Agreement +has terminated shall be considered copyright infringement and subject Customer +to applicable remedies for copyright infringement. Sections 2, 5, 6, 8 and 9 +shall survive expiration or termination of this Agreement for any reason. + +11. OWNERSHIP. As between the parties, the Software and all worldwide intellectual +property rights and proprietary rights relating thereto or embodied therein, +are the exclusive property of Stream.io and its suppliers. Stream.io and its +suppliers reserve all rights in and to the Software not expressly granted to +Customer in this Agreement, and no other licenses or rights are granted by +implication, estoppel or otherwise. + +12. WARRANTY DISCLAIMER. USE OF THIS SOFTWARE IS ENTIRELY AT YOURS AND CUSTOMER’S +OWN RISK. THE SOFTWARE IS PROVIDED “AS IS” WITHOUT ANY WARRANTY OF ANY KIND +WHATSOEVER. STREAM.IO DOES NOT MAKE, AND HEREBY DISCLAIMS, ANY WARRANTY OF ANY +KIND, WHETHER EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING WITHOUT +LIMITATION, THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE, TITLE, NON-INFRINGEMENT OF THIRD-PARTY RIGHTS, RESULTS, EFFORTS, +QUALITY OR QUIET ENJOYMENT. STREAM.IO DOES NOT WARRANT THAT THE SOFTWARE IS +ERROR-FREE, WILL FUNCTION WITHOUT INTERRUPTION, WILL MEET ANY SPECIFIC NEED +THAT CUSTOMER HAS, THAT ALL DEFECTS WILL BE CORRECTED OR THAT IT IS +SUFFICIENTLY DOCUMENTED TO BE USABLE BY CUSTOMER. TO THE EXTENT THAT STREAM.IO +MAY NOT DISCLAIM ANY WARRANTY AS A MATTER OF APPLICABLE LAW, THE SCOPE AND +DURATION OF SUCH WARRANTY WILL BE THE MINIMUM PERMITTED UNDER SUCH LAW. +CUSTOMER ACKNOWLEDGES THAT IT HAS RELIED ON NO WARRANTIES OTHER THAN THE +EXPRESS WARRANTIES IN THIS AGREEMENT. + +13. LIMITATION OF LIABILITY. TO THE FULLEST EXTENT PERMISSIBLE BY LAW, STREAM.IO’S +TOTAL LIABILITY FOR ALL DAMAGES ARISING OUT OF OR RELATED TO THE SOFTWARE OR +THIS AGREEMENT, WHETHER IN CONTRACT, TORT (INCLUDING NEGLIGENCE) OR OTHERWISE, +SHALL NOT EXCEED $100. IN NO EVENT WILL STREAM.IO BE LIABLE FOR ANY INDIRECT, +CONSEQUENTIAL, EXEMPLARY, PUNITIVE, SPECIAL OR INCIDENTAL DAMAGES OF ANY KIND +WHATSOEVER, INCLUDING ANY LOST DATA AND LOST PROFITS, ARISING FROM OR RELATING +TO THE SOFTWARE EVEN IF STREAM.IO HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. CUSTOMER ACKNOWLEDGES THAT THIS PROVISION REFLECTS THE AGREED UPON +ALLOCATION OF RISK FOR THIS AGREEMENT AND THAT STREAM.IO WOULD NOT ENTER INTO +THIS AGREEMENT WITHOUT THESE LIMITATIONS ON ITS LIABILITY. + +14. General. Customer may not assign or transfer this Agreement, by operation of +law or otherwise, or any of its rights under this Agreement (including the +license rights granted to Customer) to any third party without Stream.io’s +prior written consent, which consent will not be unreasonably withheld or +delayed. Stream.io may assign this Agreement, without consent, including, but +limited to, affiliate or any successor to all or substantially all its business +or assets to which this Agreement relates, whether by merger, sale of assets, +sale of stock, reorganization or otherwise. Any attempted assignment or +transfer in violation of the foregoing will be null and void. Stream.io shall +not be liable hereunder by reason of any failure or delay in the performance of +its obligations hereunder for any cause which is beyond the reasonable control. +All notices, consents, and approvals under this Agreement must be delivered in +writing by courier, by electronic mail, or by certified or registered mail, +(postage prepaid and return receipt requested) to the other party at the +address set forth in the customer agreement between Stream.io and Customer and +will be effective upon receipt or when delivery is refused. This Agreement will +be governed by and interpreted in accordance with the laws of the State of +Colorado, without reference to its choice of laws rules. The United Nations +Convention on Contracts for the International Sale of Goods does not apply to +this Agreement. Any action or proceeding arising from or relating to this +Agreement shall be brought in a federal or state court in Denver, Colorado, and +each party irrevocably submits to the jurisdiction and venue of any such court +in any such action or proceeding. All waivers must be in writing. Any waiver or +failure to enforce any provision of this Agreement on one occasion will not be +deemed a waiver of any other provision or of such provision on any other +occasion. If any provision of this Agreement is unenforceable, such provision +will be changed and interpreted to accomplish the objectives of such provision +to the greatest extent possible under applicable law and the remaining +provisions will continue in full force and effect. Customer shall not violate +any applicable law, rule or regulation, including those regarding the export of +technical data. The headings of Sections of this Agreement are for convenience +and are not to be used in interpreting this Agreement. As used in this +Agreement, the word “including” means “including but not limited to.” This +Agreement (including all exhibits and attachments) constitutes the entire +agreement between the parties regarding the subject hereof and supersedes all +prior or contemporaneous agreements, understandings and communication, whether +written or oral. This Agreement may be amended only by a written document +signed by both parties. The terms of any purchase order or similar document +submitted by Customer to Stream.io will have no effect. diff --git a/packages/dart_client/README.md b/packages/dart_client/README.md new file mode 100644 index 00000000..241c2f4e --- /dev/null +++ b/packages/dart_client/README.md @@ -0,0 +1,107 @@ +# Stream Chat Dart +[![Pub](https://img.shields.io/pub/v/stream_chat.svg)](https://pub.dartlang.org/packages/stream_chat) +![](https://img.shields.io/badge/platform-flutter%20%7C%20flutter%20web-ff69b4.svg?style=flat-square) +![CI](https://github.com/GetStream/stream-chat-dart/workflows/CI/badge.svg?branch=master) +[![codecov](https://codecov.io/gh/GetStream/stream-chat-dart/branch/master/graph/badge.svg)](https://codecov.io/gh/GetStream/stream-chat-dart) + +stream-chat-dart is the official Dart client for Stream Chat, a service for building chat applications. This library can be used on any Dart project and on both mobile and web apps with Flutter. + +You can sign up for a Stream account at https://getstream.io/chat/ + +## Getting started + +### Add dependency + +```yaml +dependencies: + stream_chat: ^0.2.0 +``` + +You should then run `flutter packages get` + +## Example Project + +There is a detailed Flutter example project in the `example` folder. You can directly run and play on it. + +## Setup API Client + +First you need to instantiate a chat client. The Chat client will manage API call, event handling and manage the websocket connection to Stream Chat servers. You should only create the client once and re-use it across your application. + +```dart +final client = Client("stream-chat-api-key"); +``` + +### Logging + +By default the Chat Client will write all messages with level Warn or Error to stdout. + +#### Change Logging Level + +During development you might want to enable more logging information, you can change the default log level when constructing the client. + +```dart +final client = Client("stream-chat-api-key", logLevel: Level.INFO); +``` + +#### Custom Logger + +You can handle the log messages directly instead of have them written to stdout, this is very convenient if you use an error tracking tool or if you want to centralize your logs into one facility. + +```dart +myLogHandlerFunction = (LogRecord record) { + // do something with the record (ie. send it to Sentry or Fabric) +} + +final client = Client("stream-chat-api-key", logHandlerFunction: myLogHandlerFunction); +``` + +### Offline storage + +By default the library saves information about channels and messages in a SQLite DB. + +Set the property `persistenceEnabled` to false if you don't want to use the offline storage. + +## Flutter Web + +Due to Moor web (for offline storage) you need to include the sql.js library: + +```html + + + + + + + + + +``` + +You can grab the latest version of sql-wasm.js and sql-wasm.wasm [here](https://github.com/sql-js/sql.js/releases) and copy them into your `/web` folder. + +## Contributing + +### Code conventions + +- Make sure that you run `dartfmt` before commiting your code +- Make sure all public methods and functions are well documented + +### Running tests + +- run `flutter test` + +### Releasing a new version + +- update the package version on `pubspec.yaml` and `version.dart` + +- add a changelog entry on `CHANGELOG.md` + +- run `flutter pub publish` to publish the package + +### Watch models and generate JSON code + +JSON serialization relies on code generation; make sure to keep that running while you make changes to the library + +```bash +flutter pub run build_runner watch +``` diff --git a/packages/dart_client/analysis_options.yaml b/packages/dart_client/analysis_options.yaml new file mode 100644 index 00000000..87c12424 --- /dev/null +++ b/packages/dart_client/analysis_options.yaml @@ -0,0 +1,62 @@ +include: package:pedantic/analysis_options.yaml + +analyzer: + exclude: + - lib/**/*.g.dart + - example/* + - test/* + +linter: + rules: + # these rules are documented on and in the same order as + # the Dart Lint rules page to make maintenance easier + # https://github.com/dart-lang/linter/blob/master/example/all.yaml + # - always_declare_return_types + # - always_specify_types + # - annotate_overrides + # - avoid_as + - avoid_empty_else + - avoid_init_to_null + - avoid_return_types_on_setters + - avoid_web_libraries_in_flutter + - await_only_futures + - camel_case_types + - cancel_subscriptions + - close_sinks + # - comment_references # we do not presume as to what people want to reference in their dartdocs + # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204 + - control_flow_in_finally + - empty_constructor_bodies + - empty_statements + - hash_and_equals + - implementation_imports + # - invariant_booleans + # - iterable_contains_unrelated_type + - library_names + # - library_prefixes + # - list_remove_unrelated_type + # - literal_only_boolean_expressions + - non_constant_identifier_names + # - one_member_abstracts + # - only_throw_errors + # - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - prefer_is_not_empty + # - prefer_mixin # https://github.com/dart-lang/language/issues/32 + - public_member_api_docs + - slash_for_doc_comments + # - sort_constructors_first + # - sort_unnamed_constructors_first + # - super_goes_last # no longer needed w/ Dart 2 + - test_types_in_equals + - throw_in_finally + # - type_annotate_public_apis # subset of always_specify_types + - type_init_formals + # - unawaited_futures + - unnecessary_brace_in_string_interps + - unnecessary_getters_setters + - unnecessary_statements + - unrelated_type_equality_checks + - valid_regexps \ No newline at end of file diff --git a/packages/dart_client/build.yaml b/packages/dart_client/build.yaml new file mode 100644 index 00000000..ea58f519 --- /dev/null +++ b/packages/dart_client/build.yaml @@ -0,0 +1,11 @@ +targets: + $default: + builders: + moor_generator: + options: + generate_connect_constructor: true + json_serializable: + options: + explicit_to_json: true + field_rename: snake + any_map: true diff --git a/packages/dart_client/example/README.md b/packages/dart_client/example/README.md new file mode 100644 index 00000000..5bbae90f --- /dev/null +++ b/packages/dart_client/example/README.md @@ -0,0 +1 @@ +# Stream Chat Example App diff --git a/packages/dart_client/example/android/.gitignore b/packages/dart_client/example/android/.gitignore new file mode 100644 index 00000000..bc2100d8 --- /dev/null +++ b/packages/dart_client/example/android/.gitignore @@ -0,0 +1,7 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java diff --git a/packages/dart_client/example/android/app/build.gradle b/packages/dart_client/example/android/app/build.gradle new file mode 100644 index 00000000..0aa3ddb0 --- /dev/null +++ b/packages/dart_client/example/android/app/build.gradle @@ -0,0 +1,67 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 28 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.stream_chat" + minSdkVersion 16 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/packages/dart_client/example/android/app/src/debug/AndroidManifest.xml b/packages/dart_client/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..f3e724c3 --- /dev/null +++ b/packages/dart_client/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/dart_client/example/android/app/src/main/AndroidManifest.xml b/packages/dart_client/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..03a0d883 --- /dev/null +++ b/packages/dart_client/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/packages/dart_client/example/android/app/src/main/kotlin/com/example/stream_chat_dart/MainActivity.kt b/packages/dart_client/example/android/app/src/main/kotlin/com/example/stream_chat_dart/MainActivity.kt new file mode 100644 index 00000000..ad61bdd4 --- /dev/null +++ b/packages/dart_client/example/android/app/src/main/kotlin/com/example/stream_chat_dart/MainActivity.kt @@ -0,0 +1,12 @@ +package com.example.stream_chat + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugins.GeneratedPluginRegistrant + +class MainActivity: FlutterActivity() { + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine); + } +} diff --git a/packages/dart_client/example/android/app/src/main/res/drawable/launch_background.xml b/packages/dart_client/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/packages/dart_client/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/dart_client/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/dart_client/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/packages/dart_client/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/dart_client/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/packages/dart_client/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/dart_client/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/packages/dart_client/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/dart_client/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/packages/dart_client/example/android/app/src/main/res/values/styles.xml b/packages/dart_client/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..00fa4417 --- /dev/null +++ b/packages/dart_client/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/packages/dart_client/example/android/app/src/profile/AndroidManifest.xml b/packages/dart_client/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..f3e724c3 --- /dev/null +++ b/packages/dart_client/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/dart_client/example/android/build.gradle b/packages/dart_client/example/android/build.gradle new file mode 100644 index 00000000..3100ad2d --- /dev/null +++ b/packages/dart_client/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.3.50' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.5.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/dart_client/example/android/gradle.properties b/packages/dart_client/example/android/gradle.properties new file mode 100644 index 00000000..38c8d454 --- /dev/null +++ b/packages/dart_client/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/dart_client/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/dart_client/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..296b146b --- /dev/null +++ b/packages/dart_client/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/packages/dart_client/example/android/settings.gradle b/packages/dart_client/example/android/settings.gradle new file mode 100644 index 00000000..5a2f14fb --- /dev/null +++ b/packages/dart_client/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/packages/dart_client/example/ios/.gitignore b/packages/dart_client/example/ios/.gitignore new file mode 100644 index 00000000..e96ef602 --- /dev/null +++ b/packages/dart_client/example/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/dart_client/example/ios/Flutter/AppFrameworkInfo.plist b/packages/dart_client/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..6b4c0f78 --- /dev/null +++ b/packages/dart_client/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/packages/dart_client/example/ios/Flutter/Debug.xcconfig b/packages/dart_client/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..e8efba11 --- /dev/null +++ b/packages/dart_client/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/dart_client/example/ios/Flutter/Release.xcconfig b/packages/dart_client/example/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..399e9340 --- /dev/null +++ b/packages/dart_client/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/dart_client/example/ios/Runner.xcodeproj/project.pbxproj b/packages/dart_client/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..7a054ad3 --- /dev/null +++ b/packages/dart_client/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,584 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 850BCB2A5C430C0BF58BF0DD /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2BA5904E77DC05E990FD1802 /* Pods_Runner.framework */; }; + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0409BEB0CDB875557BCCDE42 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 1232C268B110F97C5CB93E3C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 14A9392BD3E7B6A07FC5103E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 2BA5904E77DC05E990FD1802 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, + 850BCB2A5C430C0BF58BF0DD /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 198533D8779F8F2800613F27 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 2BA5904E77DC05E990FD1802 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B80C3931E831B6300D905FE /* App.framework */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEBA1CF902C7004384FC /* Flutter.framework */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 9E7371E7FEDE0F7EBA0661C0 /* Pods */, + 198533D8779F8F2800613F27 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 9E7371E7FEDE0F7EBA0661C0 /* Pods */ = { + isa = PBXGroup; + children = ( + 0409BEB0CDB875557BCCDE42 /* Pods-Runner.debug.xcconfig */, + 1232C268B110F97C5CB93E3C /* Pods-Runner.release.xcconfig */, + 14A9392BD3E7B6A07FC5103E /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9233D2646E621705569E6ED8 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 11808FAFAB0708F9F9320974 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 11808FAFAB0708F9F9320974 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + }; + 9233D2646E621705569E6ED8 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.streamChatDart; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.streamChatDart; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.streamChatDart; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/dart_client/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/dart_client/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..1d526a16 --- /dev/null +++ b/packages/dart_client/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/dart_client/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/dart_client/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..a28140cf --- /dev/null +++ b/packages/dart_client/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/dart_client/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/dart_client/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/packages/dart_client/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/dart_client/example/ios/Runner/AppDelegate.swift b/packages/dart_client/example/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..70693e4a --- /dev/null +++ b/packages/dart_client/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 zcmeHN2~<R zh`|8`A_PQ1nSu(UMFx?8j8PC!!VDphaL#`F42fd#7Vlc`zIE4n%Y~eiz4y1j|NDpi z?<@|pSJ-HM`qifhf@m%MamgwK83`XpBA<+azdF#2QsT{X@z0A9Bq>~TVErigKH1~P zRX-!h-f0NJ4Mh++{D}J+K>~~rq}d%o%+4dogzXp7RxX4C>Km5XEI|PAFDmo;DFm6G zzjVoB`@qW98Yl0Kvc-9w09^PrsobmG*Eju^=3f?0o-t$U)TL1B3;sZ^!++3&bGZ!o-*6w?;oOhf z=A+Qb$scV5!RbG+&2S}BQ6YH!FKb0``VVX~T$dzzeSZ$&9=X$3)_7Z{SspSYJ!lGE z7yig_41zpQ)%5dr4ff0rh$@ky3-JLRk&DK)NEIHecf9c*?Z1bUB4%pZjQ7hD!A0r-@NF(^WKdr(LXj|=UE7?gBYGgGQV zidf2`ZT@pzXf7}!NH4q(0IMcxsUGDih(0{kRSez&z?CFA0RVXsVFw3^u=^KMtt95q z43q$b*6#uQDLoiCAF_{RFc{!H^moH_cmll#Fc^KXi{9GDl{>%+3qyfOE5;Zq|6#Hb zp^#1G+z^AXfRKaa9HK;%b3Ux~U@q?xg<2DXP%6k!3E)PA<#4$ui8eDy5|9hA5&{?v z(-;*1%(1~-NTQ`Is1_MGdQ{+i*ccd96ab$R$T3=% zw_KuNF@vI!A>>Y_2pl9L{9h1-C6H8<)J4gKI6{WzGBi<@u3P6hNsXG=bRq5c+z;Gc3VUCe;LIIFDmQAGy+=mRyF++u=drBWV8-^>0yE9N&*05XHZpPlE zxu@?8(ZNy7rm?|<+UNe0Vs6&o?l`Pt>P&WaL~M&#Eh%`rg@Mbb)J&@DA-wheQ>hRV z<(XhigZAT z>=M;URcdCaiO3d^?H<^EiEMDV+7HsTiOhoaMX%P65E<(5xMPJKxf!0u>U~uVqnPN7T!X!o@_gs3Ct1 zlZ_$5QXP4{Aj645wG_SNT&6m|O6~Tsl$q?nK*)(`{J4b=(yb^nOATtF1_aS978$x3 zx>Q@s4i3~IT*+l{@dx~Hst21fR*+5}S1@cf>&8*uLw-0^zK(+OpW?cS-YG1QBZ5q! zgTAgivzoF#`cSz&HL>Ti!!v#?36I1*l^mkrx7Y|K6L#n!-~5=d3;K<;Zqi|gpNUn_ z_^GaQDEQ*jfzh;`j&KXb66fWEk1K7vxQIMQ_#Wu_%3 z4Oeb7FJ`8I>Px;^S?)}2+4D_83gHEq>8qSQY0PVP?o)zAv3K~;R$fnwTmI-=ZLK`= zTm+0h*e+Yfr(IlH3i7gUclNH^!MU>id$Jw>O?2i0Cila#v|twub21@e{S2v}8Z13( zNDrTXZVgris|qYm<0NU(tAPouG!QF4ZNpZPkX~{tVf8xY690JqY1NVdiTtW+NqyRP zZ&;T0ikb8V{wxmFhlLTQ&?OP7 z;(z*<+?J2~z*6asSe7h`$8~Se(@t(#%?BGLVs$p``;CyvcT?7Y!{tIPva$LxCQ&4W z6v#F*);|RXvI%qnoOY&i4S*EL&h%hP3O zLsrFZhv&Hu5tF$Lx!8(hs&?!Kx5&L(fdu}UI5d*wn~A`nPUhG&Rv z2#ixiJdhSF-K2tpVL=)5UkXRuPAFrEW}7mW=uAmtVQ&pGE-&az6@#-(Te^n*lrH^m@X-ftVcwO_#7{WI)5v(?>uC9GG{lcGXYJ~Q8q zbMFl7;t+kV;|;KkBW2!P_o%Czhw&Q(nXlxK9ak&6r5t_KH8#1Mr-*0}2h8R9XNkr zto5-b7P_auqTJb(TJlmJ9xreA=6d=d)CVbYP-r4$hDn5|TIhB>SReMfh&OVLkMk-T zYf%$taLF0OqYF?V{+6Xkn>iX@TuqQ?&cN6UjC9YF&%q{Ut3zv{U2)~$>-3;Dp)*(? zg*$mu8^i=-e#acaj*T$pNowo{xiGEk$%DusaQiS!KjJH96XZ-hXv+jk%ard#fu=@Q z$AM)YWvE^{%tDfK%nD49=PI|wYu}lYVbB#a7wtN^Nml@CE@{Gv7+jo{_V?I*jkdLD zJE|jfdrmVbkfS>rN*+`#l%ZUi5_bMS<>=MBDNlpiSb_tAF|Zy`K7kcp@|d?yaTmB^ zo?(vg;B$vxS|SszusORgDg-*Uitzdi{dUV+glA~R8V(?`3GZIl^egW{a919!j#>f` znL1o_^-b`}xnU0+~KIFLQ)$Q6#ym%)(GYC`^XM*{g zv3AM5$+TtDRs%`2TyR^$(hqE7Y1b&`Jd6dS6B#hDVbJlUXcG3y*439D8MrK!2D~6gn>UD4Imctb z+IvAt0iaW73Iq$K?4}H`7wq6YkTMm`tcktXgK0lKPmh=>h+l}Y+pDtvHnG>uqBA)l zAH6BV4F}v$(o$8Gfo*PB>IuaY1*^*`OTx4|hM8jZ?B6HY;F6p4{`OcZZ(us-RVwDx zUzJrCQlp@mz1ZFiSZ*$yX3c_#h9J;yBE$2g%xjmGF4ca z&yL`nGVs!Zxsh^j6i%$a*I3ZD2SoNT`{D%mU=LKaEwbN(_J5%i-6Va?@*>=3(dQy` zOv%$_9lcy9+(t>qohkuU4r_P=R^6ME+wFu&LA9tw9RA?azGhjrVJKy&8=*qZT5Dr8g--d+S8zAyJ$1HlW3Olryt`yE zFIph~Z6oF&o64rw{>lgZISC6p^CBer9C5G6yq%?8tC+)7*d+ib^?fU!JRFxynRLEZ zj;?PwtS}Ao#9whV@KEmwQgM0TVP{hs>dg(1*DiMUOKHdQGIqa0`yZnHk9mtbPfoLx zo;^V6pKUJ!5#n`w2D&381#5#_t}AlTGEgDz$^;u;-vxDN?^#5!zN9ngytY@oTv!nc zp1Xn8uR$1Z;7vY`-<*?DfPHB;x|GUi_fI9@I9SVRv1)qETbNU_8{5U|(>Du84qP#7 z*l9Y$SgA&wGbj>R1YeT9vYjZuC@|{rajTL0f%N@>3$DFU=`lSPl=Iv;EjuGjBa$Gw zHD-;%YOE@<-!7-Mn`0WuO3oWuL6tB2cpPw~Nvuj|KM@))ixuDK`9;jGMe2d)7gHin zS<>k@!x;!TJEc#HdL#RF(`|4W+H88d4V%zlh(7#{q2d0OQX9*FW^`^_<3r$kabWAB z$9BONo5}*(%kx zOXi-yM_cmB3>inPpI~)duvZykJ@^^aWzQ=eQ&STUa}2uT@lV&WoRzkUoE`rR0)`=l zFT%f|LA9fCw>`enm$p7W^E@U7RNBtsh{_-7vVz3DtB*y#*~(L9+x9*wn8VjWw|Q~q zKFsj1Yl>;}%MG3=PY`$g$_mnyhuV&~O~u~)968$0b2!Jkd;2MtAP#ZDYw9hmK_+M$ zb3pxyYC&|CuAbtiG8HZjj?MZJBFbt`ryf+c1dXFuC z0*ZQhBzNBd*}s6K_G}(|Z_9NDV162#y%WSNe|FTDDhx)K!c(mMJh@h87@8(^YdK$&d*^WQe8Z53 z(|@MRJ$Lk-&ii74MPIs80WsOFZ(NX23oR-?As+*aq6b?~62@fSVmM-_*cb1RzZ)`5$agEiL`-E9s7{GM2?(KNPgK1(+c*|-FKoy}X(D_b#etO|YR z(BGZ)0Ntfv-7R4GHoXp?l5g#*={S1{u-QzxCGng*oWr~@X-5f~RA14b8~B+pLKvr4 zfgL|7I>jlak9>D4=(i(cqYf7#318!OSR=^`xxvI!bBlS??`xxWeg?+|>MxaIdH1U~#1tHu zB{QMR?EGRmQ_l4p6YXJ{o(hh-7Tdm>TAX380TZZZyVkqHNzjUn*_|cb?T? zt;d2s-?B#Mc>T-gvBmQZx(y_cfkXZO~{N zT6rP7SD6g~n9QJ)8F*8uHxTLCAZ{l1Y&?6v)BOJZ)=R-pY=Y=&1}jE7fQ>USS}xP#exo57uND0i*rEk@$;nLvRB@u~s^dwRf?G?_enN@$t* zbL%JO=rV(3Ju8#GqUpeE3l_Wu1lN9Y{D4uaUe`g>zlj$1ER$6S6@{m1!~V|bYkhZA z%CvrDRTkHuajMU8;&RZ&itnC~iYLW4DVkP<$}>#&(`UO>!n)Po;Mt(SY8Yb`AS9lt znbX^i?Oe9r_o=?})IHKHoQGKXsps_SE{hwrg?6dMI|^+$CeC&z@*LuF+P`7LfZ*yr+KN8B4{Nzv<`A(wyR@!|gw{zB6Ha ziwPAYh)oJ(nlqSknu(8g9N&1hu0$vFK$W#mp%>X~AU1ay+EKWcFdif{% z#4!4aoVVJ;ULmkQf!ke2}3hqxLK>eq|-d7Ly7-J9zMpT`?dxo6HdfJA|t)?qPEVBDv z{y_b?4^|YA4%WW0VZd8C(ZgQzRI5(I^)=Ub`Y#MHc@nv0w-DaJAqsbEHDWG8Ia6ju zo-iyr*sq((gEwCC&^TYBWt4_@|81?=B-?#P6NMff(*^re zYqvDuO`K@`mjm_Jd;mW_tP`3$cS?R$jR1ZN09$YO%_iBqh5ftzSpMQQtxKFU=FYmP zeY^jph+g<4>YO;U^O>-NFLn~-RqlHvnZl2yd2A{Yc1G@Ga$d+Q&(f^tnPf+Z7serIU};17+2DU_f4Z z@GaPFut27d?!YiD+QP@)T=77cR9~MK@bd~pY%X(h%L={{OIb8IQmf-!xmZkm8A0Ga zQSWONI17_ru5wpHg3jI@i9D+_Y|pCqVuHJNdHUauTD=R$JcD2K_liQisqG$(sm=k9;L* z!L?*4B~ql7uioSX$zWJ?;q-SWXRFhz2Jt4%fOHA=Bwf|RzhwqdXGr78y$J)LR7&3T zE1WWz*>GPWKZ0%|@%6=fyx)5rzUpI;bCj>3RKzNG_1w$fIFCZ&UR0(7S?g}`&Pg$M zf`SLsz8wK82Vyj7;RyKmY{a8G{2BHG%w!^T|Njr!h9TO2LaP^_f22Q1=l$QiU84ao zHe_#{S6;qrC6w~7{y(hs-?-j?lbOfgH^E=XcSgnwW*eEz{_Z<_Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 literal 0 HcmV?d00001 diff --git a/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f091b6b0bca859a3f474b03065bef75ba58a9e4c GIT binary patch literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ literal 0 HcmV?d00001 diff --git a/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0ef06e7edb86cdfe0d15b4b0d98334a86163658 GIT binary patch literal 1716 zcmds$`#;kQ7{|XelZftyR5~xW7?MLxS4^|Hw3&P7^y)@A9Fj{Xm1~_CIV^XZ%SLBn zA;!r`GqGHg=7>xrB{?psZQs88ZaedDoagm^KF{a*>G|dJWRSe^I$DNW008I^+;Kjt z>9p3GNR^I;v>5_`+91i(*G;u5|L+Bu6M=(afLjtkya#yZ175|z$pU~>2#^Z_pCZ7o z1c6UNcv2B3?; zX%qdxCXQpdKRz=#b*q0P%b&o)5ZrNZt7$fiETSK_VaY=mb4GK`#~0K#~9^ zcY!`#Af+4h?UMR-gMKOmpuYeN5P*RKF!(tb`)oe0j2BH1l?=>y#S5pMqkx6i{*=V9JF%>N8`ewGhRE(|WohnD59R^$_36{4>S zDFlPC5|k?;SPsDo87!B{6*7eqmMdU|QZ84>6)Kd9wNfh90=y=TFQay-0__>=<4pk& zYDjgIhL-jQ9o>z32K)BgAH+HxamL{ZL~ozu)Qqe@a`FpH=oQRA8=L-m-1dam(Ix2V z?du;LdMO+ooBelr^_y4{|44tmgH^2hSzPFd;U^!1p>6d|o)(-01z{i&Kj@)z-yfWQ)V#3Uo!_U}q3u`(fOs`_f^ueFii1xBNUB z6MecwJN$CqV&vhc+)b(p4NzGGEgwWNs z@*lUV6LaduZH)4_g!cE<2G6#+hJrWd5(|p1Z;YJ7ifVHv+n49btR}dq?HHDjl{m$T z!jLZcGkb&XS2OG~u%&R$(X+Z`CWec%QKt>NGYvd5g20)PU(dOn^7%@6kQb}C(%=vr z{?RP(z~C9DPnL{q^@pVw@|Vx~@3v!9dCaBtbh2EdtoNHm4kGxp>i#ct)7p|$QJs+U z-a3qtcPvhihub?wnJqEt>zC@)2suY?%-96cYCm$Q8R%-8$PZYsx3~QOLMDf(piXMm zB=<63yQk1AdOz#-qsEDX>>c)EES%$owHKue;?B3)8aRd}m~_)>SL3h2(9X;|+2#7X z+#2)NpD%qJvCQ0a-uzZLmz*ms+l*N}w)3LRQ*6>|Ub-fyptY(keUxw+)jfwF5K{L9 z|Cl_w=`!l_o><384d&?)$6Nh(GAm=4p_;{qVn#hI8lqewW7~wUlyBM-4Z|)cZr?Rh z=xZ&Ol>4(CU85ea(CZ^aO@2N18K>ftl8>2MqetAR53_JA>Fal`^)1Y--Am~UDa4th zKfCYpcXky$XSFDWBMIl(q=Mxj$iMBX=|j9P)^fDmF(5(5$|?Cx}DKEJa&XZP%OyE`*GvvYQ4PV&!g2|L^Q z?YG}tx;sY@GzMmsY`7r$P+F_YLz)(e}% zyakqFB<6|x9R#TdoP{R$>o7y(-`$$p0NxJ6?2B8tH)4^yF(WhqGZlM3=9Ibs$%U1w zWzcss*_c0=v_+^bfb`kBFsI`d;ElwiU%frgRB%qBjn@!0U2zZehBn|{%uNIKBA7n= zzE`nnwTP85{g;8AkYxA68>#muXa!G>xH22D1I*SiD~7C?7Za+9y7j1SHiuSkKK*^O zsZ==KO(Ua#?YUpXl{ViynyT#Hzk=}5X$e04O@fsMQjb}EMuPWFO0e&8(2N(29$@Vd zn1h8Yd>6z(*p^E{c(L0Lg=wVdupg!z@WG;E0k|4a%s7Up5C0c)55XVK*|x9RQeZ1J@1v9MX;>n34(i>=YE@Iur`0Vah(inE3VUFZNqf~tSz{1fz3Fsn_x4F>o(Yo;kpqvBe-sbwH(*Y zu$JOl0b83zu$JMvy<#oH^Wl>aWL*?aDwnS0iEAwC?DK@aT)GHRLhnz2WCvf3Ba;o=aY7 z2{Asu5MEjGOY4O#Ggz@@J;q*0`kd2n8I3BeNuMmYZf{}pg=jTdTCrIIYuW~luKecn z+E-pHY%ohj@uS0%^ z&(OxwPFPD$+#~`H?fMvi9geVLci(`K?Kj|w{rZ9JgthFHV+=6vMbK~0)Ea<&WY-NC zy-PnZft_k2tfeQ*SuC=nUj4H%SQ&Y$gbH4#2sT0cU0SdFs=*W*4hKGpuR1{)mV;Qf5pw4? zfiQgy0w3fC*w&Bj#{&=7033qFR*<*61B4f9K%CQvxEn&bsWJ{&winp;FP!KBj=(P6 z4Z_n4L7cS;ao2)ax?Tm|I1pH|uLpDSRVghkA_UtFFuZ0b2#>!8;>-_0ELjQSD-DRd z4im;599VHDZYtnWZGAB25W-e(2VrzEh|etsv2YoP#VbIZ{aFkwPrzJ#JvCvA*mXS& z`}Q^v9(W4GiSs}#s7BaN!WA2bniM$0J(#;MR>uIJ^uvgD3GS^%*ikdW6-!VFUU?JV zZc2)4cMsX@j z5HQ^e3BUzOdm}yC-xA%SY``k$rbfk z;CHqifhU*jfGM@DkYCecD9vl*qr58l6x<8URB=&%{!Cu3RO*MrKZ4VO}V6R0a zZw3Eg^0iKWM1dcTYZ0>N899=r6?+adUiBKPciJw}L$=1f4cs^bio&cr9baLF>6#BM z(F}EXe-`F=f_@`A7+Q&|QaZ??Txp_dB#lg!NH=t3$G8&06MFhwR=Iu*Im0s_b2B@| znW>X}sy~m#EW)&6E&!*0%}8UAS)wjt+A(io#wGI@Z2S+Ms1Cxl%YVE800007ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 literal 0 HcmV?d00001 diff --git a/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f9ed8f5cee1c98386d13b17e89f719e83555b2 GIT binary patch literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 literal 0 HcmV?d00001 diff --git a/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..75b2d164a5a98e212cca15ea7bf2ab5de5108680 GIT binary patch literal 3831 zcmVjJBgitF5mAp-i>4+KS_oR{|13AP->1TD4=w)g|)JHOx|a2Wk1Va z!k)vP$UcQ#mdj%wNQoaJ!w>jv_6&JPyutpQps?s5dmDQ>`%?Bvj>o<%kYG!YW6H-z zu`g$@mp`;qDR!51QaS}|ZToSuAGcJ7$2HF0z`ln4t!#Yg46>;vGG9N9{V@9z#}6v* zfP?}r6b{*-C*)(S>NECI_E~{QYzN5SXRmVnP<=gzP+_Sp(Aza_hKlZ{C1D&l*(7IKXxQC1Z9#6wx}YrGcn~g%;icdw>T0Rf^w0{ z$_wn1J+C0@!jCV<%Go5LA45e{5gY9PvZp8uM$=1}XDI+9m7!A95L>q>>oe0$nC->i zeexUIvq%Uk<-$>DiDb?!In)lAmtuMWxvWlk`2>4lNuhSsjAf2*2tjT`y;@d}($o)S zn(+W&hJ1p0xy@oxP%AM15->wPLp{H!k)BdBD$toBpJh+crWdsNV)qsHaqLg2_s|Ih z`8E9z{E3sA!}5aKu?T!#enD(wLw?IT?k-yWVHZ8Akz4k5(TZJN^zZgm&zM28sfTD2BYJ|Fde3Xzh;;S` z=GXTnY4Xc)8nYoz6&vF;P7{xRF-{|2Xs5>a5)@BrnQ}I(_x7Cgpx#5&Td^4Q9_FnQ zX5so*;#8-J8#c$OlA&JyPp$LKUhC~-e~Ij!L%uSMu!-VZG7Hx-L{m2DVR2i=GR(_% zCVD!4N`I)&Q5S`?P&fQZ=4#Dgt_v2-DzkT}K(9gF0L(owe-Id$Rc2qZVLqI_M_DyO z9@LC#U28_LU{;wGZ&))}0R2P4MhajKCd^K#D+JJ&JIXZ_p#@+7J9A&P<0kdRujtQ_ zOy>3=C$kgi6$0pW06KaLz!21oOryKM3ZUOWqppndxfH}QpgjEJ`j7Tzn5bk6K&@RA?vl##y z$?V~1E(!wB5rH`>3nc&@)|#<1dN2cMzzm=PGhQ|Yppne(C-Vlt450IXc`J4R0W@I7 zd1e5uW6juvO%ni(WX7BsKx3MLngO7rHO;^R5I~0^nE^9^E_eYLgiR9&KnJ)pBbfno zSVnW$0R+&6jOOsZ82}nJ126+c|%svPo;TeUku<2G7%?$oft zyaO;tVo}(W)VsTUhq^XmFi#2z%-W9a{7mXn{uzivYQ_d6b7VJG{77naW(vHt-uhnY zVN#d!JTqVh(7r-lhtXVU6o})aZbDt_;&wJVGl2FKYFBFpU-#9U)z#(A%=IVnqytR$SY-sO( z($oNE09{D^@OuYPz&w~?9>Fl5`g9u&ecFGhqX=^#fmR=we0CJw+5xna*@oHnkahk+ z9aWeE3v|An+O5%?4fA&$Fgu~H_YmqR!yIU!bFCk4!#pAj%(lI(A5n)n@Id#M)O9Yx zJU9oKy{sRAIV3=5>(s8n{8ryJ!;ho}%pn6hZKTKbqk=&m=f*UnK$zW3YQP*)pw$O* zIfLA^!-bmBl6%d_n$#tP8Zd_(XdA*z*WH|E_yILwjtI~;jK#v-6jMl^?<%Y%`gvpwv&cFb$||^v4D&V=aNy?NGo620jL3VZnA%s zH~I|qPzB~e(;p;b^gJr7Ure#7?8%F0m4vzzPy^^(q4q1OdthF}Fi*RmVZN1OwTsAP zn9CZP`FazX3^kG(KodIZ=Kty8DLTy--UKfa1$6XugS zk%6v$Kmxt6U!YMx0JQ)0qX*{CXwZZk$vEROidEc7=J-1;peNat!vS<3P-FT5po>iE z!l3R+<`#x|+_hw!HjQGV=8!q|76y8L7N8gP3$%0kfush|u0uU^?dKBaeRSBUpOZ0c z62;D&Mdn2}N}xHRFTRI?zRv=>=AjHgH}`2k4WK=#AHB)UFrR-J87GgX*x5fL^W2#d z=(%K8-oZfMO=i{aWRDg=FX}UubM4eotRDcn;OR#{3q=*?3mE3_oJ-~prjhxh%PgQT zyn)Qozaq0@o&|LEgS{Ind4Swsr;b`u185hZPOBLL<`d2%^Yp1?oL)=jnLi;Zo0ZDliTtQ^b5SmfIMe{T==zZkbvn$KTQGlbG8w}s@M3TZnde;1Am46P3juKb zl9GU&3F=q`>j!`?SyH#r@O59%@aMX^rx}Nxe<>NqpUp5=lX1ojGDIR*-D^SDuvCKF z?3$xG(gVUsBERef_YjPFl^rU9EtD{pt z0CXwpN7BN3!8>hajGaTVk-wl=9rxmfWtIhC{mheHgStLi^+Nz12a?4r(fz)?3A%at zMlvQmL<2-R)-@G1wJ0^zQK%mR=r4d{Y3fHp){nWXUL#|CqXl(+v+qDh>FkF9`eWrW zfr^D%LNfOcTNvtx0JXR35J0~Jpi2#P3Q&80w+nqNfc}&G0A~*)lGHKv=^FE+b(37|)zL;KLF>oiGfb(?&1 zV3XRu!Sw>@quKiab%g6jun#oZ%!>V#A%+lNc?q>6+VvyAn=kf_6z^(TZUa4Eelh{{ zqFX-#dY(EV@7l$NE&kv9u9BR8&Ojd#ZGJ6l8_BW}^r?DIS_rU2(XaGOK z225E@kH5Opf+CgD^{y29jD4gHbGf{1MD6ggQ&%>UG4WyPh5q_tb`{@_34B?xfSO*| zZv8!)q;^o-bz`MuxXk*G^}(6)ACb@=Lfs`Hxoh>`Y0NE8QRQ!*p|SH@{r8=%RKd4p z+#Ty^-0kb=-H-O`nAA3_6>2z(D=~Tbs(n8LHxD0`R0_ATFqp-SdY3(bZ3;VUM?J=O zKCNsxsgt@|&nKMC=*+ZqmLHhX1KHbAJs{nGVMs6~TiF%Q)P@>!koa$%oS zjXa=!5>P`vC-a}ln!uH1ooeI&v?=?v7?1n~P(wZ~0>xWxd_Aw;+}9#eULM7M8&E?Y zC-ZLhi3RoM92SXUb-5i-Lmt5_rfjE{6y^+24`y$1lywLyHO!)Boa7438K4#iLe?rh z2O~YGSgFUBH?og*6=r9rme=peP~ah`(8Zt7V)j5!V0KPFf_mebo3z95U8(up$-+EA^9dTRLq>Yl)YMBuch9%=e5B`Vnb>o zt03=kq;k2TgGe4|lGne&zJa~h(UGutjP_zr?a7~#b)@15XNA>Dj(m=gg2Q5V4-$)D|Q9}R#002ovPDHLkV1o7DH3k3x literal 0 HcmV?d00001 diff --git a/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/dart_client/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..c4df70d39da7941ef3f6dcb7f06a192d8dcb308d GIT binary patch literal 1888 zcmV-m2cP(fP)x~L`~4d)Rspd&<9kFh{hn*KP1LP0~$;u(LfAu zp%fx&qLBcRHx$G|3q(bv@+b;o0*D|jwD-Q9uQR(l*ST}s+uPgQ-MeFwZ#GS?b332? z&Tk$&_miXn3IGq)AmQ)3sisq{raD4(k*bHvpCe-TdWq^NRTEVM)i9xbgQ&ccnUVx* zEY%vS%gDcSg=!tuIK8$Th2_((_h^+7;R|G{n06&O2#6%LK`a}n?h_fL18btz<@lFG za}xS}u?#DBMB> zw^b($1Z)`9G?eP95EKi&$eOy@K%h;ryrR3la%;>|o*>CgB(s>dDcNOXg}CK9SPmD? zmr-s{0wRmxUnbDrYfRvnZ@d z6johZ2sMX{YkGSKWd}m|@V7`Degt-43=2M?+jR%8{(H$&MLLmS;-|JxnX2pnz;el1jsvqQz}pGSF<`mqEXRQ5sC4#BbwnB_4` zc5bFE-Gb#JV3tox9fp-vVEN{(tOCpRse`S+@)?%pz+zVJXSooTrNCUg`R6`hxwb{) zC@{O6MKY8tfZ5@!yy=p5Y|#+myRL=^{tc(6YgAnkg3I(Cd!r5l;|;l-MQ8B`;*SCE z{u)uP^C$lOPM z5d~UhKhRRmvv{LIa^|oavk1$QiEApSrP@~Jjbg`<*dW4TO?4qG%a%sTPUFz(QtW5( zM)lA+5)0TvH~aBaOAs|}?u2FO;yc-CZ1gNM1dAxJ?%m?YsGR`}-xk2*dxC}r5j$d* zE!#Vtbo69h>V4V`BL%_&$} z+oJAo@jQ^Tk`;%xw-4G>hhb&)B?##U+(6Fi7nno`C<|#PVA%$Y{}N-?(Gc$1%tr4Pc}}hm~yY#fTOe!@v9s-ik$dX~|ygArPhByaXn8 zpI^FUjNWMsTFKTP3X7m?UK)3m zp6rI^_zxRYrx6_QmhoWoDR`fp4R7gu6;gdO)!KexaoO2D88F9x#TM1(9Bn7g;|?|o z)~$n&Lh#hCP6_LOPD>a)NmhW})LADx2kq=X7}7wYRj-0?dXr&bHaRWCfSqvzFa=sn z-8^gSyn-RmH=BZ{AJZ~!8n5621GbUJV7Qvs%JNv&$%Q17s_X%s-41vAPfIR>;x0Wlqr5?09S>x#%Qkt>?(&XjFRY}*L6BeQ3 z<6XEBh^S7>AbwGm@XP{RkeEKj6@_o%oV?hDuUpUJ+r#JZO?!IUc;r0R?>mi)*ZpQ) z#((dn=A#i_&EQn|hd)N$#A*fjBFuiHcYvo?@y1 z5|fV=a^a~d!c-%ZbMNqkMKiSzM{Yq=7_c&1H!mXk60Uv32dV;vMg&-kQ)Q{+PFtwc zj|-uQ;b^gts??J*9VxxOro}W~Q9j4Em|zSRv)(WSO9$F$s=Ydu%Q+5DOid~lwk&we zY%W(Z@ofdwPHncEZzZgmqS|!gTj3wQq9rxQy+^eNYKr1mj&?tm@wkO*9@UtnRMG>c aR{jt9+;fr}hV%pg00001^@s67{VYS000c7NklQEG_j zup^)eW&WUIApqy$=APz8jE@awGp)!bsTjDbrJO`$x^ZR^dr;>)LW>{ zs70vpsD38v)19rI=GNk1b(0?Js9~rjsQsu*K;@SD40RB-3^gKU-MYC7G!Bw{fZsqp zih4iIi;Hr_xZ033Iu{sQxLS=}yBXgLMn40d++>aQ0#%8D1EbGZp7+ z5=mK?t31BkVYbGOxE9`i748x`YgCMwL$qMsChbSGSE1`p{nSmadR zcQ#R)(?!~dmtD0+D2!K zR9%!Xp1oOJzm(vbLvT^$IKp@+W2=-}qTzTgVtQ!#Y7Gxz}stUIm<1;oBQ^Sh2X{F4ibaOOx;5ZGSNK z0maF^@(UtV$=p6DXLgRURwF95C=|U8?osGhgOED*b z7woJ_PWXBD>V-NjQAm{~T%sjyJ{5tn2f{G%?J!KRSrrGvQ1(^`YLA5B!~eycY(e5_ z*%aa{at13SxC(=7JT7$IQF~R3sy`Nn%EMv!$-8ZEAryB*yB1k&stni)=)8-ODo41g zkJu~roIgAih94tb=YsL%iH5@^b~kU9M-=aqgXIrbtxMpFy5mekFm#edF9z7RQ6V}R zBIhbXs~pMzt0VWy1Fi$^fh+1xxLDoK09&5&MJl(q#THjPm(0=z2H2Yfm^a&E)V+a5 zbi>08u;bJsDRUKR9(INSc7XyuWv(JsD+BB*0hS)FO&l&7MdViuur@-<-EHw>kHRGY zqoT}3fDv2-m{NhBG8X}+rgOEZ;amh*DqN?jEfQdqxdj08`Sr=C-KmT)qU1 z+9Cl)a1mgXxhQiHVB}l`m;-RpmKy?0*|yl?FXvJkFxuu!fKlcmz$kN(a}i*saM3nr z0!;a~_%Xqy24IxA2rz<+08=B-Q|2PT)O4;EaxP^6qixOv7-cRh?*T?zZU`{nIM-at zTKYWr9rJ=tppQ9I#Z#mLgINVB!pO-^FOcvFw6NhV0gztuO?g ztoA*C-52Q-Z-P#xB4HAY3KQVd%dz1S4PA3vHp0aa=zAO?FCt zC_GaTyVBg2F!bBr3U@Zy2iJgIAt>1sf$JWA9kh{;L+P*HfUBX1Zy{4MgNbDfBV_ly z!y#+753arsZUt@366jIC0klaC@ckuk!qu=pAyf7&QmiBUT^L1&tOHzsK)4n|pmrVT zs2($4=?s~VejTFHbFdDOwG;_58LkIj1Fh@{glkO#F1>a==ymJS$z;gdedT1zPx4Kj ztjS`y_C}%af-RtpehdQDt3a<=W5C4$)9W@QAse;WUry$WYmr51ml9lkeunUrE`-3e zmq1SgSOPNEE-Mf+AGJ$g0M;3@w!$Ej;hMh=v=I+Lpz^n%Pg^MgwyqOkNyu2c^of)C z1~ALor3}}+RiF*K4+4{(1%1j3pif1>sv0r^mTZ?5Jd-It!tfPfiG_p$AY*Vfak%FG z4z#;wLtw&E&?}w+eKG^=#jF7HQzr8rV0mY<1YAJ_uGz~$E13p?F^fPSzXSn$8UcI$ z8er9{5w5iv0qf8%70zV71T1IBB1N}R5Kp%NO0=5wJalZt8;xYp;b{1K) zHY>2wW-`Sl{=NpR%iu3(u6l&)rc%%cSA#aV7WCowfbFR4wcc{LQZv~o1u_`}EJA3>ki`?9CKYTA!rhO)if*zRdd}Kn zEPfYbhoVE~!FI_2YbC5qAj1kq;xP6%J8+?2PAs?`V3}nyFVD#sV3+uP`pi}{$l9U^ zSz}_M9f7RgnnRhaoIJgT8us!1aB&4!*vYF07Hp&}L zCRlop0oK4DL@ISz{2_BPlezc;xj2|I z23RlDNpi9LgTG_#(w%cMaS)%N`e>~1&a3<{Xy}>?WbF>OOLuO+j&hc^YohQ$4F&ze z+hwnro1puQjnKm;vFG~o>`kCeUIlkA-2tI?WBKCFLMBY=J{hpSsQ=PDtU$=duS_hq zHpymHt^uuV1q@uc4bFb{MdG*|VoW@15Osrqt2@8ll0qO=j*uOXn{M0UJX#SUztui9FN4)K3{9!y8PC-AHHvpVTU;x|-7P+taAtyglk#rjlH2 z5Gq8ik}BPaGiM{#Woyg;*&N9R2{J0V+WGB69cEtH7F?U~Kbi6ksi*`CFXsi931q7Y zGO82?whBhN%w1iDetv%~wM*Y;E^)@Vl?VDj-f*RX>{;o_=$fU!&KAXbuadYZ46Zbg z&6jMF=49$uL^73y;;N5jaHYv)BTyfh&`qVLYn?`o6BCA_z-0niZz=qPG!vonK3MW_ zo$V96zM!+kJRs{P-5-rQVse0VBH*n6A58)4uc&gfHMa{gIhV2fGf{st>E8sKyP-$8zp~wJX^A*@DI&-;8>gANXZj zU)R+Y)PB?=)a|Kj>8NXEu^S_h^7R`~Q&7*Kn!xyvzVv&^>?^iu;S~R2e-2fJx-oUb cX)(b1KSk$MOV07*qoM6N<$f&6$jw%VRuvdN2+38CZWny1cRtlsl+0_KtW)EU14Ei(F!UtWuj4IK+3{sK@>rh zs1Z;=(DD&U6+tlyL?UnHVN^&g6QhFi2#HS+*qz;(>63G(`|jRtW|nz$Pv7qTovP!^ zP_jES{mr@O-02w%!^a?^1ZP!_KmQiz0L~jZ=W@Qt`8wzOoclQsAS<5YdH;a(4bGLE zk8s}1If(PSIgVi!XE!5kA?~z*sobvNyohr;=Q_@h2@$6Flyej3J)D-6YfheRGl`HEcPk|~huT_2-U?PfL=4BPV)f1o!%rQ!NMt_MYw-5bUSwQ9Z&zC>u zOrl~UJglJNa%f50Ok}?WB{on`Ci`p^Y!xBA?m@rcJXLxtrE0FhRF3d*ir>yzO|BD$ z3V}HpFcCh6bTzY}Nt_(W%QYd3NG)jJ4<`F<1Od) zfQblTdC&h2lCz`>y?>|9o2CdvC8qZeIZt%jN;B7Hdn2l*k4M4MFEtq`q_#5?}c$b$pf_3y{Y!cRDafZBEj-*OD|gz#PBDeu3QoueOesLzB+O zxjf2wvf6Wwz>@AiOo2mO4=TkAV+g~%_n&R;)l#!cBxjuoD$aS-`IIJv7cdX%2{WT7 zOm%5rs(wqyPE^k5SIpUZ!&Lq4<~%{*>_Hu$2|~Xa;iX*tz8~G6O3uFOS?+)tWtdi| zV2b#;zRN!m@H&jd=!$7YY6_}|=!IU@=SjvGDFtL;aCtw06U;-v^0%k0FOyESt z1Wv$={b_H&8FiRV?MrzoHWd>%v6KTRU;-v^Miiz+@q`(BoT!+<37CKhoKb)|8!+RG z6BQFU^@fRW;s8!mOf2QViKQGk0TVER6EG1`#;Nm39Do^PoT!+<37AD!%oJe86(=et zZ~|sLzU>V-qYiU6V8$0GmU7_K8|Fd0B?+9Un1BhKAz#V~Fk^`mJtlCX#{^8^M8!me z8Yg;8-~>!e<-iG;h*0B1kBKm}hItVGY6WnjVpgnTTAC$rqQ^v)4KvOtpY|sIj@WYg zyw##ZZ5AC2IKNC;^hwg9BPk0wLStlmBr;E|$5GoAo$&Ui_;S9WY62n3)i49|T%C#i017z3J=$RF|KyZWnci*@lW4 z=AKhNN6+m`Q!V3Ye68|8y@%=am>YD0nG99M)NWc20%)gwO!96j7muR}Fr&54SxKP2 zP30S~lt=a*qDlbu3+Av57=9v&vr<6g0&`!8E2fq>I|EJGKs}t|{h7+KT@)LfIV-3K zK)r_fr2?}FFyn*MYoLC>oV-J~eavL2ho4a4^r{E-8m2hi>~hA?_vIG4a*KT;2eyl1 zh_hUvUJpNCFwBvRq5BI*srSle>c6%n`#VNsyC|MGa{(P&08p=C9+WUw9Hl<1o9T4M zdD=_C0F7#o8A_bRR?sFNmU0R6tW`ElnF8p53IdHo#S9(JoZCz}fHwJ6F<&?qrpVqE zte|m%89JQD+XwaPU#%#lVs-@-OL);|MdfINd6!XwP2h(eyafTUsoRkA%&@fe?9m@jw-v(yTTiV2(*fthQH9}SqmsRPVnwwbV$1E(_lkmo&S zF-truCU914_$jpqjr(>Ha4HkM4YMT>m~NosUu&UZ>zirfHo%N6PPs9^_o$WqPA0#5 z%tG>qFCL+b*0s?sZ;Sht0nE7Kl>OVXy=gjWxxK;OJ3yGd7-pZf7JYNcZo2*1SF`u6 zHJyRRxGw9mDlOiXqVMsNe#WX`fC`vrtjSQ%KmLcl(lC>ZOQzG^%iql2w-f_K@r?OE zwCICifM#L-HJyc7Gm>Ern?+Sk3&|Khmu4(~3qa$(m6Ub^U0E5RHq49za|XklN#?kP zl;EstdW?(_4D>kwjWy2f!LM)y?F94kyU3`W!6+AyId-89v}sXJpuic^NLL7GJItl~ zsiuB98AI-(#Mnm|=A-R6&2fwJ0JVSY#Q>&3$zFh|@;#%0qeF=j5Ajq@4i0tIIW z&}sk$&fGwoJpe&u-JeGLi^r?dO`m=y(QO{@h zQqAC7$rvz&5+mo3IqE?h=a~6m>%r5Quapvzq;{y~p zJpyXOBgD9VrW7@#p6l7O?o3feml(DtSL>D^R) zZUY%T2b0-vBAFN7VB;M88!~HuOXi4KcI6aRQ&h|XQ0A?m%j2=l1f0cGP}h(oVfJ`N zz#PpmFC*ieab)zJK<4?^k=g%OjPnkANzbAbmGZHoVRk*mTfm75s_cWVa`l*f$B@xu z5E*?&@seIo#*Y~1rBm!7sF9~~u6Wrj5oICUOuz}CS)jdNIznfzCA(stJ(7$c^e5wN z?lt>eYgbA!kvAR7zYSD&*r1$b|(@;9dcZ^67R0 zXAXJKa|5Sdmj!g578Nwt6d$sXuc&MWezA0Whd`94$h{{?1IwXP4)Tx4obDK%xoFZ_Z zjjHJ_P@R_e5blG@yEjnaJb`l;s%Lb2&=8$&Ct-fV`E^4CUs)=jTk!I}2d&n!f@)bm z@ z_4Dc86+3l2*p|~;o-Sb~oXb_RuLmoifDU^&Te$*FevycC0*nE3Xws8gsWp|Rj2>SM zns)qcYj?^2sd8?N!_w~4v+f-HCF|a$TNZDoNl$I1Uq87euoNgKb6&r26TNrfkUa@o zfdiFA@p{K&mH3b8i!lcoz)V{n8Q@g(vR4ns4r6w;K z>1~ecQR0-<^J|Ndg5fvVUM9g;lbu-){#ghGw(fg>L zh)T5Ljb%lWE;V9L!;Cqk>AV1(rULYF07ZBJbGb9qbSoLAd;in9{)95YqX$J43-dY7YU*k~vrM25 zxh5_IqO0LYZW%oxQ5HOzmk4x{atE*vipUk}sh88$b2tn?!ujEHn`tQLe&vo}nMb&{ zio`xzZ&GG6&ZyN3jnaQy#iVqXE9VT(3tWY$n-)uWDQ|tc{`?fq2F`oQ{;d3aWPg4Hp-(iE{ry>MIPWL> iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/dart_client/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/dart_client/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/dart_client/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/dart_client/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/dart_client/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/dart_client/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/packages/dart_client/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/dart_client/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/dart_client/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/packages/dart_client/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/dart_client/example/ios/Runner/Base.lproj/Main.storyboard b/packages/dart_client/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/packages/dart_client/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/dart_client/example/ios/Runner/Info.plist b/packages/dart_client/example/ios/Runner/Info.plist new file mode 100644 index 00000000..d89e1ca3 --- /dev/null +++ b/packages/dart_client/example/ios/Runner/Info.plist @@ -0,0 +1,50 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + stream_chat + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/dart_client/example/ios/Runner/Runner-Bridging-Header.h b/packages/dart_client/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/packages/dart_client/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/dart_client/example/lib/channel_header.dart b/packages/dart_client/example/lib/channel_header.dart new file mode 100644 index 00000000..c452a2f5 --- /dev/null +++ b/packages/dart_client/example/lib/channel_header.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:timeago/timeago.dart' as timeago; + +import './channel_name_text.dart'; +import 'channel_image.dart'; +import 'stream_channel.dart'; + +class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { + final bool showBackButton; + final VoidCallback onBackPressed; + + ChannelHeader({ + Key key, + this.showBackButton = true, + this.onBackPressed, + }) : preferredSize = Size.fromHeight(kToolbarHeight), + super(key: key); + + @override + Widget build(BuildContext context) { + final streamChat = StreamChannel.of(context); + return AppBar( + leading: showBackButton ? _buildBackButton(context) : Container(), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 10.0), + child: ChannelImage(channel: streamChat.channel), + ), + ], + centerTitle: true, + title: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ChannelNameText( + channel: streamChat.channel, + ), + _buildLastActive(context, streamChat.channel), + ], + ), + ); + } + + StatelessWidget _buildLastActive(BuildContext context, Channel channel) { + return (channel.lastMessageAt != null) + ? Text( + 'Active ${timeago.format(channel.lastMessageAt)}', + style: Theme.of(context).textTheme.caption, + ) + : Container(); + } + + Padding _buildBackButton(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(14.0), + child: RawMaterialButton( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + disabledElevation: 0, + hoverElevation: 0, + onPressed: () { + if (onBackPressed != null) { + onBackPressed(); + } else { + Navigator.of(context).pop(); + } + }, + fillColor: Colors.black.withOpacity(.1), + padding: EdgeInsets.all(4), + child: Icon( + Icons.arrow_back_ios, + size: 15, + color: Colors.black, + ), + ), + ); + } + + @override + final Size preferredSize; +} diff --git a/packages/dart_client/example/lib/channel_image.dart b/packages/dart_client/example/lib/channel_image.dart new file mode 100644 index 00000000..f28db180 --- /dev/null +++ b/packages/dart_client/example/lib/channel_image.dart @@ -0,0 +1,25 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; + +class ChannelImage extends StatelessWidget { + const ChannelImage({ + Key key, + @required this.channel, + }) : super(key: key); + + final Channel channel; + + @override + Widget build(BuildContext context) { + return CircleAvatar( + radius: 20, + backgroundImage: channel.extraData.containsKey('image') + ? CachedNetworkImageProvider(channel.extraData['image'] as String) + : null, + child: channel.extraData.containsKey('image') + ? null + : Text(channel.config.name[0]), + ); + } +} diff --git a/packages/dart_client/example/lib/channel_list_page.dart b/packages/dart_client/example/lib/channel_list_page.dart new file mode 100644 index 00000000..7c3864b5 --- /dev/null +++ b/packages/dart_client/example/lib/channel_list_page.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import './connection_indicator.dart'; +import './message_page.dart'; +import './stream_channel.dart'; +import 'channel_list_view.dart'; +import 'channel_page_app_bar.dart'; +import 'stream_chat.dart'; + +class ChannelListPage extends StatefulWidget { + ChannelListPage({ + this.filter, + this.options, + this.sort, + this.pagination, + }); + + final Map filter; + final Map options; + final List sort; + final PaginationParams pagination; + + @override + ChannelListPageState createState() => ChannelListPageState(); +} + +class ChannelListPageState extends State { + String _selectedChannelId; + bool showSplit; + IndicatorController _indicatorController = IndicatorController(); + + @override + Widget build(BuildContext context) { + showSplit = MediaQuery.of(context).size.width > 1000; + return Flex( + direction: Axis.horizontal, + children: [ + Flexible( + flex: 1, + child: Scaffold( + bottomNavigationBar: ConnectionIndicator( + indicatorController: _indicatorController, + ), + appBar: ChannelPageAppBar(), + body: ChannelListView( + channelWidget: MessagePage(), + options: widget.options, + filter: widget.filter, + pagination: widget.pagination, + sort: widget.sort, + onChannelTap: showSplit + ? (channelClient, _) { + _navigateToChannel(context, channelClient); + } + : null, + ), + floatingActionButton: FloatingActionButton( + onPressed: () {}, + backgroundColor: Colors.white, + child: Icon( + Icons.send, + ), + ), + ), + ), + showSplit ? _buildMessageView(context) : Container(), + ], + ); + } + + Flexible _buildMessageView(BuildContext context) { + return Flexible( + flex: 2, + child: _selectedChannelId == null + ? Scaffold( + body: Center( + child: Text( + 'Pick a channel to show the messages 💬', + style: Theme.of(context).textTheme.headline, + ), + ), + ) + : StreamChannel( + channelClient: StreamChat.of(context) + .client + .state + .channels + .firstWhere((c) => c.id == _selectedChannelId), + child: MessagePage(), + ), + ); + } + + void _navigateToChannel( + BuildContext context, + Channel channel, + ) { + setState(() { + _selectedChannelId = channel.id; + }); + } + + @override + void initState() { + super.initState(); + + final streamChat = StreamChat.of(context); + streamChat.client.wsConnectionStatus.addListener(() { + if (streamChat.client.wsConnectionStatus.value == + ConnectionStatus.disconnected) { + _indicatorController.showIndicator( + duration: Duration(minutes: 1), + color: Colors.red, + text: 'Disconnected', + ); + } else if (streamChat.client.wsConnectionStatus.value == + ConnectionStatus.connecting) { + _indicatorController.showIndicator( + duration: Duration(minutes: 1), + color: Colors.yellow, + text: 'Reconnecting', + ); + } else if (streamChat.client.wsConnectionStatus.value == + ConnectionStatus.connected) { + _indicatorController.showIndicator( + duration: Duration(seconds: 5), + color: Colors.green, + text: 'Connected', + ); + streamChat.clearChannels(); + + streamChat.queryChannels(); + } + }); + } +} diff --git a/packages/dart_client/example/lib/channel_list_view.dart b/packages/dart_client/example/lib/channel_list_view.dart new file mode 100644 index 00000000..9c89d4d6 --- /dev/null +++ b/packages/dart_client/example/lib/channel_list_view.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'channel_preview.dart'; +import 'stream_channel.dart'; +import 'stream_chat.dart'; + +typedef ChannelTapCallback = void Function(Channel, Widget); + +class ChannelListView extends StatefulWidget { + ChannelListView({ + Key key, + this.filter, + this.options, + this.sort, + this.pagination, + this.onChannelTap, + this.channelWidget, + this.channelPreview, + }) : assert(channelWidget != null || onChannelTap != null), + super(key: key); + + final Map filter; + final Map options; + final List sort; + final PaginationParams pagination; + final ScrollController _scrollController = ScrollController(); + final ChannelTapCallback onChannelTap; + final Widget channelWidget; + final Widget channelPreview; + + @override + _ChannelListViewState createState() => _ChannelListViewState(); +} + +class _ChannelListViewState extends State { + @override + Widget build(BuildContext context) { + final streamChat = StreamChat.of(context); + return RefreshIndicator( + onRefresh: () async { + streamChat.clearChannels(); + return streamChat.queryChannels( + filter: widget.filter, + sortOptions: widget.sort, + paginationParams: widget.pagination, + options: widget.options, + ); + }, + child: StreamBuilder>( + stream: streamChat.client.state.channelsStream, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } + + if (snapshot.hasError) { + print((snapshot.error as Error).stackTrace); + return Center( + child: Text(snapshot.error.toString()), + ); + } + + final channelsStates = snapshot.data; + return ListView.custom( + physics: AlwaysScrollableScrollPhysics(), + controller: widget._scrollController, + childrenDelegate: SliverChildBuilderDelegate( + (context, i) { + return _itemBuilder(context, i, channelsStates); + }, + childCount: (channelsStates.length * 2) + 1, + findChildIndexCallback: (key) { + final ValueKey valueKey = key; + final index = channelsStates + .indexWhere((cs) => 'CHANNEL-${cs.id}' == valueKey.value); + return index != -1 ? (index * 2) : null; + }, + ), + ); + }), + ); + } + + Widget _itemBuilder(context, int i, List channelsStates) { + if (i % 2 != 0) { + return _separatorBuilder(context, i); + } + + i = i ~/ 2; + + final streamChat = StreamChat.of(context); + if (i < channelsStates.length) { + final channelState = channelsStates[i]; + + final channelClient = streamChat.client.state.channels + .firstWhere((c) => c.cid == channelState.cid); + + ChannelTapCallback onTap; + if (widget.onChannelTap != null) { + onTap = widget.onChannelTap; + } else { + onTap = (client, _) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return StreamChannel( + child: widget.channelWidget, + channelClient: client, + ); + }, + ), + ); + }; + } + + Widget child; + if (widget.channelPreview != null) { + child = Stack( + children: [ + widget.channelPreview, + Positioned.fill( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + onTap(channelClient, widget.channelWidget); + }, + ), + ), + ), + ], + ); + } else { + child = ChannelPreview( + onTap: (channelClient) { + onTap(channelClient, widget.channelWidget); + }, + ); + } + + return StreamChannel( + key: ValueKey('CHANNEL-${channelClient?.id}'), + child: child, + channelClient: channelClient, + ); + } else { + return _buildQueryProgressIndicator(context, streamChat); + } + } + + Widget _buildQueryProgressIndicator(context, StreamChat streamChat) { + return StreamBuilder( + stream: streamChat.queryChannelsLoading, + initialData: false, + builder: (context, snapshot) { + return Container( + height: 100, + padding: EdgeInsets.all(32), + child: Center( + child: snapshot.data ? CircularProgressIndicator() : Container(), + ), + ); + }); + } + + Widget _separatorBuilder(context, i) { + return Container( + height: 1, + color: Colors.black.withOpacity(0.1), + margin: EdgeInsets.symmetric(horizontal: 16), + ); + } + + void _listenChannelPagination(StreamChat streamChat) { + if (widget._scrollController.position.maxScrollExtent == + widget._scrollController.position.pixels) { + streamChat.queryChannels( + filter: widget.filter, + sortOptions: widget.sort, + paginationParams: widget.pagination.copyWith( + offset: streamChat.channels.length, + ), + options: widget.options, + ); + } + } + + @override + void initState() { + super.initState(); + + final streamChat = StreamChat.of(context); + streamChat.queryChannels( + filter: widget.filter, + sortOptions: widget.sort, + paginationParams: widget.pagination, + options: widget.options, + ); + + widget._scrollController.addListener(() { + _listenChannelPagination(streamChat); + }); + } +} diff --git a/packages/dart_client/example/lib/channel_name_text.dart b/packages/dart_client/example/lib/channel_name_text.dart new file mode 100644 index 00000000..f31f38c5 --- /dev/null +++ b/packages/dart_client/example/lib/channel_name_text.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; + +class ChannelNameText extends StatelessWidget { + const ChannelNameText({ + Key key, + this.channel, + }) : super(key: key); + + final Channel channel; + + @override + Widget build(BuildContext context) { + return Text( + channel.extraData['name'] as String ?? channel.config.name, + style: Theme.of(context).textTheme.body2, + ); + } +} diff --git a/packages/dart_client/example/lib/channel_page_app_bar.dart b/packages/dart_client/example/lib/channel_page_app_bar.dart new file mode 100644 index 00000000..8ee1e754 --- /dev/null +++ b/packages/dart_client/example/lib/channel_page_app_bar.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +class ChannelPageAppBar extends StatelessWidget implements PreferredSizeWidget { + ChannelPageAppBar({ + Key key, + }) : preferredSize = Size.fromHeight(kToolbarHeight), + super(key: key); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Material( + elevation: 4, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 5, horizontal: 30), + child: Container( + decoration: BoxDecoration( + color: Colors.black.withAlpha(5), + borderRadius: BorderRadius.circular(32.0), + border: Border.all(color: Colors.black.withOpacity(.2))), + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: TextField( + style: Theme.of(context).textTheme.body1, + autofocus: false, + decoration: InputDecoration( + hintText: 'Search', + prefixText: ' ', + border: InputBorder.none, + ), + ), + ), + ), + ), + ), + ); + } + + @override + final Size preferredSize; +} diff --git a/packages/dart_client/example/lib/channel_preview.dart b/packages/dart_client/example/lib/channel_preview.dart new file mode 100644 index 00000000..8926e107 --- /dev/null +++ b/packages/dart_client/example/lib/channel_preview.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'channel_image.dart'; +import 'channel_name_text.dart'; +import 'stream_channel.dart'; +import 'stream_chat.dart'; + +class ChannelPreview extends StatelessWidget { + final void Function(Channel) onTap; + + const ChannelPreview({ + Key key, + this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final streamChannel = StreamChannel.of(context); + return _buildChannelPreview( + context, + streamChannel, + ); + } + + StreamChannel _buildChannelPreview( + BuildContext context, + StreamChannelState streamChannel, + ) { + final channelClient = StreamChat.of(context) + .client + .state + .channels + .firstWhere((c) => c.cid == streamChannel.channel.cid); + return StreamChannel( + channelClient: channelClient, + child: ListTile( + onTap: () { + onTap(channelClient); + }, + leading: ChannelImage( + channel: streamChannel.channel, + ), + title: ChannelNameText( + channel: streamChannel.channel, + ), + subtitle: _buildSubtitle( + streamChannel, + ), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _buildDate(context, streamChannel.channel.lastMessageAt), + ], + ), + ), + ); + } + + Text _buildDate(BuildContext context, DateTime lastMessageAt) { + String stringDate; + final now = DateTime.now(); + + if (now.year != lastMessageAt.year || + now.month != lastMessageAt.month || + now.day != lastMessageAt.day) { + stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy'); + } else { + stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm'); + } + + return Text( + stringDate, + style: Theme.of(context).textTheme.caption, + ); + } + + Widget _buildSubtitle( + StreamChannelState streamChannel, + ) { + return StreamBuilder>( + stream: streamChannel.channelClient.state.typingEventsStream, + initialData: [], + builder: (context, snapshot) { + final typings = snapshot.data; + final opacity = + streamChannel.channelClient.state.unreadCount > .0 ? 1.0 : 0.5; + return typings.isNotEmpty + ? _buildTypings(typings, context, opacity) + : _buildLastMessage(context, streamChannel, opacity); + }); + } + + Widget _buildLastMessage( + BuildContext context, StreamChannelState streamChannel, double opacity) { + final lastMessage = streamChannel.channel.state.messages.isNotEmpty + ? streamChannel.channel.state.messages.last + : null; + if (lastMessage == null) { + return SizedBox.fromSize( + size: Size.zero, + ); + } + + final prefix = lastMessage.attachments + .map((e) { + if (e.type == 'image') { + return '📷'; + } else if (e.type == 'video') { + return '🎬'; + } + return null; + }) + .where((e) => e != null) + .join(' '); + + return Text( + '$prefix ${lastMessage.text ?? ''}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.caption.copyWith( + color: Colors.black.withOpacity(opacity), + ), + ); + } + + Text _buildTypings(List typings, BuildContext context, double opacity) { + return Text( + '${typings.map((u) => u.extraData.containsKey('name') ? u.extraData['name'] : u.id).join(',')} ${typings.length == 1 ? 'is' : 'are'} typing...', + maxLines: 1, + style: Theme.of(context).textTheme.caption.copyWith( + color: Colors.black.withOpacity(opacity), + ), + ); + } +} diff --git a/packages/dart_client/example/lib/connection_indicator.dart b/packages/dart_client/example/lib/connection_indicator.dart new file mode 100644 index 00000000..4fb81861 --- /dev/null +++ b/packages/dart_client/example/lib/connection_indicator.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; + +class ConnectionIndicator extends StatefulWidget { + final IndicatorController indicatorController; + + const ConnectionIndicator({ + Key key, + this.indicatorController, + }) : super(key: key); + + @override + _ConnectionIndicatorState createState() => _ConnectionIndicatorState(); +} + +class _ConnectionIndicatorState extends State { + double _height = 0; + String _text; + Color _color; + VoidCallback _listener; + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: Duration(milliseconds: 300), + height: _height, + child: Container( + width: double.infinity, + child: Material( + color: _color ?? Theme.of(context).snackBarTheme.backgroundColor, + child: Center( + child: Text(_text ?? ''), + ), + ), + ), + ); + } + + @override + void initState() { + super.initState(); + + _listener = () { + final values = widget.indicatorController.indicatorValues.value; + if (mounted) { + setState(() { + _height = 30; + _text = values.text; + _color = values.color; + }); + } + + if (values.duration != null) { + Future.delayed(values.duration, () { + if (mounted) { + setState(() { + _height = 0; + _text = ''; + }); + } + }); + } + }; + + widget.indicatorController.indicatorValues.addListener(_listener); + } + + @override + void dispose() { + widget.indicatorController.indicatorValues.removeListener(_listener); + super.dispose(); + } +} + +class IndicatorValues { + final String text; + final Color color; + final Duration duration; + + IndicatorValues({ + this.duration, + this.text, + this.color, + }); +} + +class IndicatorController { + ValueNotifier indicatorValues = ValueNotifier(null); + + void showIndicator({ + Duration duration, + Color color, + String text, + }) { + indicatorValues.value = IndicatorValues( + text: text, + color: color, + duration: duration, + ); + } +} diff --git a/packages/dart_client/example/lib/main.dart b/packages/dart_client/example/lib/main.dart new file mode 100644 index 00000000..de87783b --- /dev/null +++ b/packages/dart_client/example/lib/main.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'channel_list_page.dart'; +import 'stream_chat.dart'; + +void main() async { + final client = Client( + "qk4nn7rpcn75", + logLevel: Level.INFO, + ); + + await client.setUser( + User(id: "wild-breeze-7"), + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoid2lsZC1icmVlemUtNyJ9.VM2EX1EXOfgqa-bTH_3JzeY0T99ngWzWahSauP3dBMo', + ); + + runApp(StreamChat( + child: MyApp(), + client: client, + )); +} + +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Stream Chat Example', + home: ChatLoader(), + theme: ThemeData( + scaffoldBackgroundColor: Color(0xfff1f1f3), + primaryColor: Color(0xfff1f1f3), + accentColor: Color(0xff006bff), + iconTheme: IconThemeData( + color: Color(0xff006bff), + ), + floatingActionButtonTheme: FloatingActionButtonThemeData( + foregroundColor: Color(0xff006bff), + ), + backgroundColor: Color(0xfff1f1f3), + canvasColor: Color(0xfff1f1f3), + ), + ); + } + + @override + void dispose() { + StreamChat.of(context).dispose(); + super.dispose(); + } +} + +class ChatLoader extends StatelessWidget { + @override + Widget build(BuildContext context) { + final streamChat = StreamChat.of(context); + return StreamBuilder( + stream: streamChat.userStream, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Scaffold( + body: Center( + child: Text('${snapshot.error}'), + ), + ); + } else if (!snapshot.hasData) { + return Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } else { + return ChannelListPage( + filter: { + 'members': { + '\$in': [StreamChat.of(context).user.id], + } + }, + sort: [SortOption("last_message_at")], + pagination: PaginationParams( + limit: 20, + ), + ); + } + }, + ); + } +} diff --git a/packages/dart_client/example/lib/message_input.dart b/packages/dart_client/example/lib/message_input.dart new file mode 100644 index 00000000..243f1d31 --- /dev/null +++ b/packages/dart_client/example/lib/message_input.dart @@ -0,0 +1,137 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'stream_channel.dart'; + +class MessageInput extends StatefulWidget { + MessageInput({ + Key key, + this.onMessageSent, + this.parent, + }) : super(key: key); + + final void Function(Message) onMessageSent; + final Message parent; + + @override + _MessageInputState createState() => _MessageInputState(); +} + +class _MessageInputState extends State { + final _textController = TextEditingController(); + bool _messageIsPresent = false; + bool _typingStarted = false; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + width: MediaQuery.of(context).size.width, + padding: EdgeInsets.all(2), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + gradient: _typingStarted + ? LinearGradient(colors: [Color(0xFF00AEFF), Color(0xFF0076FF)]) + : null, + ), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).canvasColor, + borderRadius: BorderRadius.circular(10.0), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.black.withAlpha(5), + borderRadius: BorderRadius.circular(10.0), + border: Border.all(color: Colors.black.withOpacity(.2)), + ), + child: Flex( + direction: Axis.horizontal, + children: [ + Expanded( + child: TextField( + minLines: null, + maxLines: null, + onSubmitted: (_) { + _sendMessage(context); + }, + controller: _textController, + onChanged: (s) { + StreamChannel.of(context).channelClient.keyStroke(); + setState(() { + _messageIsPresent = s.trim().isNotEmpty; + }); + }, + onTap: () { + setState(() { + _typingStarted = true; + }); + }, + style: Theme.of(context).textTheme.body1, + autofocus: false, + decoration: InputDecoration( + hintText: 'Write a message', + prefixText: ' ', + border: InputBorder.none, + ), + ), + ), + AnimatedCrossFade( + crossFadeState: _messageIsPresent + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: _buildSendButton(context), + secondChild: Container(), + duration: Duration(milliseconds: 300), + alignment: Alignment.center, + ), + ], + ), + ), + ), + ), + ); + } + + IconButton _buildSendButton(BuildContext context) { + return IconButton( + onPressed: () { + _sendMessage(context); + }, + icon: Icon( + Icons.send, + ), + ); + } + + void _sendMessage(BuildContext context) { + final text = _textController.text.trim(); + if (text.isEmpty) { + return; + } + + _textController.clear(); + setState(() { + _messageIsPresent = false; + _typingStarted = false; + }); + FocusScope.of(context).unfocus(); + + StreamChannel.of(context) + .channelClient + .sendMessage( + Message( + parentId: widget.parent?.id, + text: text, + ), + ) + .then((_) { + if (widget.onMessageSent != null) { + widget.onMessageSent(Message(text: text)); + } + }); + } +} diff --git a/packages/dart_client/example/lib/message_list_view.dart b/packages/dart_client/example/lib/message_list_view.dart new file mode 100644 index 00000000..af8608d6 --- /dev/null +++ b/packages/dart_client/example/lib/message_list_view.dart @@ -0,0 +1,280 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_widgets/flutter_widgets.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'message_widget.dart'; +import 'stream_channel.dart'; + +typedef MessageBuilder = Widget Function(BuildContext, Message, int index); +typedef ParentMessageBuilder = Widget Function(BuildContext, Message); +typedef OnThreadSelectCallback = void Function(Message parent); + +class MessageListView extends StatefulWidget { + MessageListView({ + Key key, + MessageBuilder messageBuilder, + this.parentMessageBuilder, + this.parentMessage, + this.onThreadSelect, + }) : _messageBuilder = messageBuilder, + super(key: key); + + final MessageBuilder _messageBuilder; + final ParentMessageBuilder parentMessageBuilder; + final OnThreadSelectCallback onThreadSelect; + final Message parentMessage; + + @override + _MessageListViewState createState() => _MessageListViewState(); +} + +class _MessageListViewState extends State { + static const _newMessageLoadingOffset = 100; + final ScrollController _scrollController = ScrollController(); + bool _isBottom = true; + bool _topWasVisible = false; + List _messages = []; + List _newMessageList = []; + + @override + Widget build(BuildContext context) { + final streamChannel = StreamChannel.of(context); + + /// TODO: find a better solution when (https://github.com/flutter/flutter/issues/21023) is fixed + return NotificationListener( + onNotification: (_) { + if (_scrollController.offset < 150 && _newMessageList.isNotEmpty) { + setState(() { + _messages.insertAll(0, _newMessageList); + _newMessageList.clear(); + }); + } + return true; + }, + child: ListView.custom( + physics: AlwaysScrollableScrollPhysics(), + controller: _scrollController, + reverse: true, + childrenDelegate: SliverChildBuilderDelegate( + (context, i) { + if (i == _messages.length + 1) { + if (widget.parentMessage != null) { + if (widget.parentMessageBuilder != null) { + return widget.parentMessageBuilder( + context, widget.parentMessage); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + MessageWidget( + key: ValueKey( + 'PARENT-MESSAGE-${widget.parentMessage.id}'), + previousMessage: null, + message: widget.parentMessage.copyWith(replyCount: 0), + nextMessage: null, + onThreadSelect: widget.onThreadSelect, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Container( + padding: const EdgeInsets.all(8), + child: Text( + 'Start of a new thread', + textAlign: TextAlign.center, + ), + color: Theme.of(context).primaryColorLight, + ), + ), + ], + ); + } + } else { + return SizedBox(); + } + } + + if (i == _messages.length) { + return _buildLoadingIndicator(streamChannel); + } + final message = _messages[i]; + + if (widget._messageBuilder != null) { + return widget._messageBuilder(context, message, i); + } + + final previousMessage = + i < _messages.length - 1 ? _messages[i + 1] : null; + final nextMessage = i > 0 ? _messages[i - 1] : null; + + if (i == 0) { + return _buildBottomMessage( + streamChannel, + previousMessage, + message, + context, + ); + } + + if (i == _messages.length - 1) { + return _buildTopMessage( + message, + nextMessage, + streamChannel, + context, + ); + } + + return MessageWidget( + key: ValueKey('MESSAGE-${message.id}'), + previousMessage: previousMessage, + message: message, + nextMessage: nextMessage, + onThreadSelect: widget.onThreadSelect, + ); + }, + childCount: _messages.length + 2, + findChildIndexCallback: (key) { + final ValueKey valueKey = key; + final index = _messages + .indexWhere((m) => 'MESSAGE-${m.id}' == valueKey.value); + return index != -1 ? index : null; + }, + ), + ), + ); + } + + Container _buildLoadingIndicator(StreamChannelState streamChannel) { + return Container( + height: 50, + child: StreamBuilder( + stream: streamChannel.queryMessage, + initialData: false, + builder: (context, snapshot) { + if (snapshot.hasError) { + print((snapshot.error as Error).stackTrace.toString()); + return Center( + child: Text(snapshot.error.toString()), + ); + } + if (!snapshot.data) { + return Container(); + } + return Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: CircularProgressIndicator(), + ), + ); + }), + ); + } + + Widget _buildTopMessage( + Message message, + Message nextMessage, + StreamChannelState streamChannelState, + BuildContext context, + ) { + return VisibilityDetector( + key: ValueKey('TOP-MESSAGE'), + child: MessageWidget( + key: ValueKey('MESSAGE-${message.id}'), + previousMessage: null, + message: message, + nextMessage: nextMessage, + onThreadSelect: widget.onThreadSelect, + ), + onVisibilityChanged: (visibility) { + final topIsVisible = visibility.visibleBounds != Rect.zero; + if (topIsVisible && !_topWasVisible) { + streamChannelState.queryMessages(); + } + _topWasVisible = topIsVisible; + }, + ); + } + + Widget _buildBottomMessage( + StreamChannelState channelBloc, + Message previousMessage, + Message message, + BuildContext context, + ) { + return VisibilityDetector( + key: ValueKey('BOTTOM-MESSAGE'), + onVisibilityChanged: (visibility) { + _isBottom = visibility.visibleBounds != Rect.zero; + if (_isBottom) { + if (channelBloc.channelClient.state.unreadCount > 0) { + channelBloc.channelClient.markRead(); + } + } + }, + child: MessageWidget( + key: ValueKey('MESSAGE-${message.id}'), + previousMessage: previousMessage, + message: message, + nextMessage: null, + onThreadSelect: widget.onThreadSelect, + ), + ); + } + + StreamSubscription _streamListener; + + @override + void initState() { + super.initState(); + + final streamChannel = StreamChannel.of(context); + if (streamChannel.channelClient.state.unreadCount > 0) { + streamChannel.channelClient.markRead(); + } + + Stream> stream; + + if (widget.parentMessage == null) { + stream = streamChannel.channelStateStream.map((c) => c.messages); + } else { + streamChannel.getReplies(widget.parentMessage.id); + stream = streamChannel.channelClient.state.threadsStream + .where((threads) => threads.containsKey(widget.parentMessage.id)) + .map((threads) => threads[widget.parentMessage.id]); + } + + _streamListener = stream.listen((newMessages) { + newMessages = newMessages.reversed.toList(); + if (_messages.isEmpty || newMessages.first.id != _messages.first.id) { + if (!_scrollController.hasClients || + _scrollController.offset < _newMessageLoadingOffset) { + setState(() { + _messages = newMessages; + }); + } else if (newMessages.first.user.id == + streamChannel.channelClient.client.state.user.id) { + _scrollController.jumpTo(0); + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _messages = newMessages; + }); + }); + } else { + _newMessageList = newMessages; + } + } else { + setState(() { + _messages = newMessages; + }); + } + }); + } + + @override + void dispose() { + _streamListener.cancel(); + super.dispose(); + } +} diff --git a/packages/dart_client/example/lib/message_page.dart b/packages/dart_client/example/lib/message_page.dart new file mode 100644 index 00000000..3d3c8073 --- /dev/null +++ b/packages/dart_client/example/lib/message_page.dart @@ -0,0 +1,110 @@ +import 'package:animations/animations.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import './channel_header.dart'; +import 'connection_indicator.dart'; +import 'message_input.dart'; +import 'message_list_view.dart'; +import 'stream_channel.dart'; +import 'stream_chat.dart'; + +class MessagePage extends StatefulWidget { + final PreferredSizeWidget _channelHeader; + + const MessagePage({ + Key key, + PreferredSizeWidget channelHeader, + this.parentMessage, + }) : _channelHeader = channelHeader, + super(key: key); + + final Message parentMessage; + + @override + _MessagePageState createState() => _MessagePageState(); +} + +class _MessagePageState extends State { + IndicatorController _indicatorController = IndicatorController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: true, + appBar: widget._channelHeader ?? ChannelHeader(), + body: Column( + children: [ + ConnectionIndicator( + indicatorController: _indicatorController, + ), + Expanded( + child: MessageListView( + parentMessage: widget.parentMessage, + onThreadSelect: (message) { + Navigator.of(context).push( + PageRouteBuilder( + pageBuilder: (_, __, ___) => StreamChannel( + channelClient: StreamChannel.of(context).channelClient, + child: MessagePage( + parentMessage: message, + channelHeader: widget._channelHeader, + ), + ), + transitionsBuilder: ( + _, + animation, + secondaryAnimation, + child, + ) => + SharedAxisTransition( + child: child, + animation: animation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.horizontal, + ), + ), + ); + }, + ), + ), + MessageInput(), + ], + ), + ); + } + + @override + void initState() { + super.initState(); + + final streamChat = StreamChat.of(context); + streamChat.client.wsConnectionStatus.addListener(() { + if (streamChat.client.wsConnectionStatus.value == + ConnectionStatus.disconnected) { + _indicatorController.showIndicator( + duration: Duration(minutes: 1), + color: Colors.red, + text: 'Disconnected', + ); + } else if (streamChat.client.wsConnectionStatus.value == + ConnectionStatus.connecting) { + _indicatorController.showIndicator( + duration: Duration(minutes: 1), + color: Colors.yellow, + text: 'Reconnecting', + ); + } else if (streamChat.client.wsConnectionStatus.value == + ConnectionStatus.connected) { + _indicatorController.showIndicator( + duration: Duration(seconds: 5), + color: Colors.green, + text: 'Connected', + ); + + final streamChat = StreamChannel.of(context); + streamChat.queryMessages(); + } + }); + } +} diff --git a/packages/dart_client/example/lib/message_widget.dart b/packages/dart_client/example/lib/message_widget.dart new file mode 100644 index 00000000..aba9ced1 --- /dev/null +++ b/packages/dart_client/example/lib/message_widget.dart @@ -0,0 +1,353 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:chewie/chewie.dart'; +import 'package:date_format/date_format.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:video_player/video_player.dart'; + +import 'message_list_view.dart'; +import 'stream_chat.dart'; +import 'user_avatar.dart'; + +class MessageWidget extends StatefulWidget { + const MessageWidget({ + Key key, + @required this.previousMessage, + @required this.message, + @required this.nextMessage, + this.onThreadSelect, + }) : super(key: key); + + final Message previousMessage; + final Message message; + final Message nextMessage; + final OnThreadSelectCallback onThreadSelect; + + @override + _MessageWidgetState createState() => _MessageWidgetState(); +} + +class _MessageWidgetState extends State + with AutomaticKeepAliveClientMixin { + final Map _videoControllers = {}; + final Map _chuwieControllers = {}; + + @override + Widget build(BuildContext context) { + super.build(context); + + final streamChat = StreamChat.of(context); + final currentUserId = streamChat.user.id; + final messageUserId = widget.message.user.id; + final previousUserId = widget.previousMessage?.user?.id; + final nextUserId = widget.nextMessage?.user?.id; + final isMyMessage = messageUserId == currentUserId; + final isLastUser = previousUserId == messageUserId; + final isNextUser = nextUserId == messageUserId; + final alignment = + isMyMessage ? Alignment.centerRight : Alignment.centerLeft; + + var row = [ + Column( + crossAxisAlignment: + isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + _buildBubble(context, isMyMessage, isLastUser), + widget.message.replyCount > 0 + ? GestureDetector( + onTap: () { + if (widget.onThreadSelect != null) { + widget.onThreadSelect(widget.message); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + children: [ + Text( + 'Replies: ${widget.message.replyCount}', + style: Theme.of(context) + .textTheme + .subtitle + .copyWith(color: Colors.blue), + ), + Icon( + Icons.subdirectory_arrow_left, + color: Colors.black12, + ), + ], + ), + ), + ) + : Container(), + isNextUser ? Container() : _buildTimestamp(isMyMessage, alignment), + ], + ), + isNextUser + ? Container( + width: 40, + ) + : Padding( + padding: EdgeInsets.only( + left: isMyMessage ? 8.0 : 0, + right: isMyMessage ? 0 : 8.0, + ), + child: UserAvatar(user: widget.message.user), + ), + ]; + + if (!isMyMessage) { + row = row.reversed.toList(); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + margin: EdgeInsets.only( + top: isLastUser ? 5 : 24, + bottom: widget.nextMessage == null ? 30 : 0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: + isMyMessage ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: row, + ), + ); + } + + Widget _buildBubble( + BuildContext context, + bool isMyMessage, + bool isLastUser, + ) { + var nOfAttachmentWidgets = 0; + + final column = Column( + crossAxisAlignment: + isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: widget.message.attachments.map((attachment) { + nOfAttachmentWidgets++; + + Widget attachmentWidget; + if (attachment.type == 'video') { + attachmentWidget = _buildVideo(attachment, isMyMessage, isLastUser); + } else if (attachment.type == 'image' || attachment.type == 'giphy') { + attachmentWidget = _buildImage(isMyMessage, isLastUser, attachment); + } + + if (attachmentWidget != null) { + final boxDecoration = _buildBoxDecoration(isMyMessage, isLastUser) + .copyWith(color: Color(0xffebebeb)); + return ClipRRect( + borderRadius: boxDecoration.borderRadius, + child: Container( + decoration: boxDecoration, + constraints: BoxConstraints.loose(Size.fromWidth(300)), + child: Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + attachmentWidget, + attachment.title != null + ? Container( + constraints: + BoxConstraints.loose(Size.fromHeight(70)), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + attachment.title, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .subtitle + .copyWith(color: Colors.blue), + ), + Text( + Uri.parse(attachment.thumbUrl) + .authority + .split('.') + .reversed + .take(2) + .toList() + .reversed + .join('.'), + overflow: TextOverflow.ellipsis, + style: + Theme.of(context).textTheme.caption, + ), + ], + ), + ), + color: Color(0xffebebeb), + ) + : Container(), + ], + ), + attachment.type == 'image' && attachment.titleLink != null + ? Positioned.fill( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _launchURL(attachment.titleLink), + ), + ), + ) + : SizedBox.fromSize( + size: Size.zero, + ), + ], + ), + margin: EdgeInsets.only( + top: nOfAttachmentWidgets > 1 ? 5 : 0, + ), + ), + ); + } + + nOfAttachmentWidgets--; + return Container(); + }).toList(), + ); + + if (widget.message.text.trim().isNotEmpty) { + column.children.add(Container( + margin: EdgeInsets.only( + top: nOfAttachmentWidgets > 0 ? 5 : 0, + ), + decoration: _buildBoxDecoration( + isMyMessage, isLastUser || nOfAttachmentWidgets > 0), + padding: EdgeInsets.all(10), + constraints: BoxConstraints.loose(Size.fromWidth(300)), + child: MarkdownBody( + data: '${widget.message.text}', + onTapLink: (link) { + _launchURL(link); + }, + styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)), + ), + )); + } + + return column; + } + + Widget _buildImage( + bool isMyMessage, + bool isLastUser, + Attachment attachment, + ) { + return CachedNetworkImage( + imageUrl: attachment.thumbUrl ?? attachment.imageUrl, + fit: BoxFit.cover, + ); + } + + Widget _buildVideo( + Attachment attachment, + bool isMyMessage, + bool isLastUser, + ) { + VideoPlayerController videoController; + if (_videoControllers.containsKey(attachment.assetUrl)) { + videoController = _videoControllers[attachment.assetUrl]; + } else { + videoController = VideoPlayerController.network(attachment.assetUrl); + _videoControllers[attachment.assetUrl] = videoController; + } + + ChewieController chewieController; + if (_chuwieControllers.containsKey(attachment.assetUrl)) { + chewieController = _chuwieControllers[attachment.assetUrl]; + } else { + chewieController = ChewieController( + videoPlayerController: videoController, + autoInitialize: true, + errorBuilder: (_, e) { + return Stack( + children: [ + Container( + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.cover, + image: CachedNetworkImageProvider( + attachment.thumbUrl, + ), + ), + ), + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _launchURL(attachment.titleLink), + ), + ), + ], + ); + }); + _chuwieControllers[attachment.assetUrl] = chewieController; + } + + return Chewie( + key: ValueKey( + 'ATTACHMENT-${attachment.title}-${widget.message.id}'), + controller: chewieController, + ); + } + + Future _launchURL(String url) async { + if (await canLaunch(url)) { + await launch(url); + } else { + Scaffold.of(context).showSnackBar( + SnackBar( + content: Text('Cannot launch the url'), + ), + ); + } + } + + @override + void dispose() { + _videoControllers.values.forEach((element) { + element.dispose(); + }); + super.dispose(); + } + + Widget _buildTimestamp(bool isMyMessage, Alignment alignment) { + return Padding( + padding: const EdgeInsets.only(top: 5.0), + child: Text( + formatDate(widget.message.createdAt.toLocal(), [HH, ':', nn]), + ), + ); + } + + BoxDecoration _buildBoxDecoration(bool isMyMessage, bool isLastUser) { + return BoxDecoration( + border: isMyMessage ? null : Border.all(color: Colors.black.withAlpha(8)), + borderRadius: BorderRadius.only( + topLeft: Radius.circular((isMyMessage || !isLastUser) ? 16 : 2), + bottomLeft: Radius.circular(isMyMessage ? 16 : 2), + topRight: Radius.circular((isMyMessage && isLastUser) ? 2 : 16), + bottomRight: Radius.circular(isMyMessage ? 2 : 16), + ), + color: isMyMessage ? Color(0xffebebeb) : Colors.white, + ); + } + + @override + bool get wantKeepAlive { + return widget.message.attachments.isNotEmpty; + } +} diff --git a/packages/dart_client/example/lib/stream_channel.dart b/packages/dart_client/example/lib/stream_channel.dart new file mode 100644 index 00000000..40c082c1 --- /dev/null +++ b/packages/dart_client/example/lib/stream_channel.dart @@ -0,0 +1,128 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:stream_chat/stream_chat.dart'; + +class StreamChannel extends StatefulWidget { + StreamChannel({ + Key key, + @required this.child, + @required this.channelClient, + }) : super( + key: key, + ); + + final Widget child; + final Channel channelClient; + + static StreamChannelState of(BuildContext context) { + StreamChannelState streamChannelState; + + streamChannelState = context.findAncestorStateOfType(); + + if (streamChannelState == null) { + throw Exception( + 'You must have a StreamChannel widget at the top of your widget tree'); + } + + return streamChannelState; + } + + @override + StreamChannelState createState() => StreamChannelState(); +} + +class StreamChannelState extends State { + StreamChannelState(); + + Channel get channelClient => widget.channelClient; + + Channel get channel => widget.channelClient; + + Stream get channelStateStream => + widget.channelClient.state.channelStateStream; + + final BehaviorSubject _queryMessageController = BehaviorSubject(); + + Stream get queryMessage => _queryMessageController.stream; + + void queryMessages() { + _queryMessageController.add(true); + + String firstId; + if (channel.state.messages.isNotEmpty) { + firstId = channel.state.messages.first.id; + } + + widget.channelClient + .query( + messagesPagination: PaginationParams( + lessThan: firstId, + limit: 100, + ), + ) + .then((res) { + _queryMessageController.add(false); + }).catchError((e, stack) { + _queryMessageController.addError(e, stack); + }); + } + + Future getReplies(String parentId) async { + _queryMessageController.add(true); + + String firstId; + if (widget.channelClient.state.threads.containsKey(parentId)) { + firstId = widget.channelClient.state.threads[parentId].first.id; + } + + return widget.channelClient + .getReplies( + parentId, + PaginationParams( + lessThan: firstId, + limit: 100, + ), + ) + .then((res) { + _queryMessageController.add(false); + }).catchError((e, stack) { + _queryMessageController.addError(e, stack); + }); + } + + @override + void dispose() { + _queryMessageController.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.channelClient == null) { + return Center( + child: CircularProgressIndicator(), + ); + } + return FutureBuilder( + future: widget.channelClient.initialized, + initialData: widget.channelClient.state != null, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } else if (snapshot.hasError) { + return Center( + child: Text(snapshot.error), + ); + } else { + return widget.child; + } + }, + ); + } +} diff --git a/packages/dart_client/example/lib/stream_chat.dart b/packages/dart_client/example/lib/stream_chat.dart new file mode 100644 index 00000000..f6b1db38 --- /dev/null +++ b/packages/dart_client/example/lib/stream_chat.dart @@ -0,0 +1,95 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:stream_chat/stream_chat.dart'; + +class StreamChat extends InheritedWidget { + final Client client; + final List _subscriptions = []; + + StreamChat({ + Key key, + @required this.client, + @required Widget child, + }) : super( + key: key, + child: child, + ) { + _subscriptions.add(client.on(EventType.messageNew).listen((Event e) { + final index = channels.indexWhere((c) => c.cid == e.cid); + if (index > 0) { + final channel = channels.removeAt(index); + channels.insert(0, channel); + } + })); + } + + User get user => client.state.user; + + Stream get userStream => client.state.userStream; + + final List channels = []; + + final BehaviorSubject _queryChannelsLoadingController = + BehaviorSubject.seeded(false); + + Stream get queryChannelsLoading => + _queryChannelsLoadingController.stream; + + Future queryChannels({ + Map filter, + List sortOptions, + PaginationParams paginationParams, + Map options, + }) async { + if (_queryChannelsLoadingController.value) { + return; + } + _queryChannelsLoadingController.sink.add(true); + + try { + client.queryChannels( + filter: filter, + sort: sortOptions, + options: options, + paginationParams: paginationParams, + ); + } finally { + _queryChannelsLoadingController.sink.add(false); + } + } + + void clearChannels() { + channels.clear(); + } + + void dispose() { + client.dispose(); + _subscriptions.forEach((s) => s.cancel()); + _queryChannelsLoadingController.close(); + } + + @override + bool updateShouldNotify(InheritedWidget oldWidget) { + return true; + } + + static StreamChat of(BuildContext context, [bool listen = false]) { + StreamChat streamChat; + + if (listen) { + streamChat = context.dependOnInheritedWidgetOfExactType(); + } else { + streamChat = context.findAncestorWidgetOfExactType(); + } + + if (streamChat == null) { + throw Exception( + 'You must have a StreamChat widget at the top of your widget tree'); + } + + return streamChat; + } +} diff --git a/packages/dart_client/example/lib/user_avatar.dart b/packages/dart_client/example/lib/user_avatar.dart new file mode 100644 index 00000000..c815f363 --- /dev/null +++ b/packages/dart_client/example/lib/user_avatar.dart @@ -0,0 +1,29 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; + +class UserAvatar extends StatelessWidget { + const UserAvatar({ + Key key, + @required this.user, + this.radius = 16, + }) : super(key: key); + + final User user; + final double radius; + + @override + Widget build(BuildContext context) { + return CircleAvatar( + radius: this.radius, + backgroundImage: user.extraData.containsKey('image') + ? CachedNetworkImageProvider(user.extraData['image'] as String) + : null, + child: user.extraData.containsKey('image') + ? null + : Text(user?.extraData?.containsKey('name') ?? false + ? user.extraData['name'][0] + : ''), + ); + } +} diff --git a/packages/dart_client/example/pubspec.yaml b/packages/dart_client/example/pubspec.yaml new file mode 100644 index 00000000..59a8bb96 --- /dev/null +++ b/packages/dart_client/example/pubspec.yaml @@ -0,0 +1,33 @@ +name: stream_chat_example +description: example Flutter project using Stream Chat service + +version: 1.0.0+1 + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_widgets: ^0.1.11 + timeago: ^2.0.26 + jiffy: ^3.0.1 + date_format: ^1.0.8 + cached_network_image: ^2.0.0 + flutter_markdown: ^0.3.4 + url_launcher: ^5.4.2 + video_player: ^0.10.8+1 + chewie: ^0.9.10 + animations: ^1.0.0+5 + + stream_chat: + path: ../ + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: any + build_web_compilers: any + +flutter: + uses-material-design: true diff --git a/packages/dart_client/example/test/widget_test.dart b/packages/dart_client/example/test/widget_test.dart new file mode 100644 index 00000000..8d7438e9 --- /dev/null +++ b/packages/dart_client/example/test/widget_test.dart @@ -0,0 +1,29 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. 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:stream_chat_example/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(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); + }); +} diff --git a/packages/dart_client/example/web/icons/Icon-192.png b/packages/dart_client/example/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 zcmZ`-2T+sGz6~)*FVZ`aW+(v>MIm&M-g^@e2u-B-DoB?qO+b1Tq<5uCCv>ESfRum& zp%X;f!~1{tzL__3=gjVJ=j=J>+nMj%ncXj1Q(b|Ckbw{Y0FWpt%4y%$uD=Z*c-x~o zE;IoE;xa#7Ll5nj-e4CuXB&G*IM~D21rCP$*xLXAK8rIMCSHuSu%bL&S3)8YI~vyp@KBu9Ph7R_pvKQ@xv>NQ`dZp(u{Z8K3yOB zn7-AR+d2JkW)KiGx0hosml;+eCXp6+w%@STjFY*CJ?udJ64&{BCbuebcuH;}(($@@ znNlgBA@ZXB)mcl9nbX#F!f_5Z=W>0kh|UVWnf!At4V*LQP%*gPdCXd6P@J4Td;!Ur z<2ZLmwr(NG`u#gDEMP19UcSzRTL@HsK+PnIXbVBT@oHm53DZr?~V(0{rsalAfwgo zEh=GviaqkF;}F_5-yA!1u3!gxaR&Mj)hLuj5Q-N-@Lra{%<4ONja8pycD90&>yMB` zchhd>0CsH`^|&TstH-8+R`CfoWqmTTF_0?zDOY`E`b)cVi!$4xA@oO;SyOjJyP^_j zx^@Gdf+w|FW@DMdOi8=4+LJl$#@R&&=UM`)G!y%6ZzQLoSL%*KE8IO0~&5XYR9 z&N)?goEiWA(YoRfT{06&D6Yuu@Qt&XVbuW@COb;>SP9~aRc+z`m`80pB2o%`#{xD@ zI3RAlukL5L>px6b?QW1Ac_0>ew%NM!XB2(H+1Y3AJC?C?O`GGs`331Nd4ZvG~bMo{lh~GeL zSL|tT*fF-HXxXYtfu5z+T5Mx9OdP7J4g%@oeC2FaWO1D{=NvL|DNZ}GO?O3`+H*SI z=grGv=7dL{+oY0eJFGO!Qe(e2F?CHW(i!!XkGo2tUvsQ)I9ev`H&=;`N%Z{L zO?vV%rDv$y(@1Yj@xfr7Kzr<~0{^T8wM80xf7IGQF_S-2c0)0D6b0~yD7BsCy+(zL z#N~%&e4iAwi4F$&dI7x6cE|B{f@lY5epaDh=2-(4N05VO~A zQT3hanGy_&p+7Fb^I#ewGsjyCEUmSCaP6JDB*=_()FgQ(-pZ28-{qx~2foO4%pM9e z*_63RT8XjgiaWY|*xydf;8MKLd{HnfZ2kM%iq}fstImB-K6A79B~YoPVa@tYN@T_$ zea+9)<%?=Fl!kd(Y!G(-o}ko28hg2!MR-o5BEa_72uj7Mrc&{lRh3u2%Y=Xk9^-qa zBPWaD=2qcuJ&@Tf6ue&)4_V*45=zWk@Z}Q?f5)*z)-+E|-yC4fs5CE6L_PH3=zI8p z*Z3!it{1e5_^(sF*v=0{`U9C741&lub89gdhKp|Y8CeC{_{wYK-LSbp{h)b~9^j!s z7e?Y{Z3pZv0J)(VL=g>l;<}xk=T*O5YR|hg0eg4u98f2IrA-MY+StQIuK-(*J6TRR z|IM(%uI~?`wsfyO6Tgmsy1b3a)j6M&-jgUjVg+mP*oTKdHg?5E`!r`7AE_#?Fc)&a z08KCq>Gc=ne{PCbRvs6gVW|tKdcE1#7C4e`M|j$C5EYZ~Y=jUtc zj`+?p4ba3uy7><7wIokM79jPza``{Lx0)zGWg;FW1^NKY+GpEi=rHJ+fVRGfXO zPHV52k?jxei_!YYAw1HIz}y8ZMwdZqU%ESwMn7~t zdI5%B;U7RF=jzRz^NuY9nM)&<%M>x>0(e$GpU9th%rHiZsIT>_qp%V~ILlyt^V`=d z!1+DX@ah?RnB$X!0xpTA0}lN@9V-ePx>wQ?-xrJr^qDlw?#O(RsXeAvM%}rg0NT#t z!CsT;-vB=B87ShG`GwO;OEbeL;a}LIu=&@9cb~Rsx(ZPNQ!NT7H{@j0e(DiLea>QD zPmpe90gEKHEZ8oQ@6%E7k-Ptn#z)b9NbD@_GTxEhbS+}Bb74WUaRy{w;E|MgDAvHw zL)ycgM7mB?XVh^OzbC?LKFMotw3r@i&VdUV%^Efdib)3@soX%vWCbnOyt@Y4swW925@bt45y0HY3YI~BnnzZYrinFy;L?2D3BAL`UQ zEj))+f>H7~g8*VuWQ83EtGcx`hun$QvuurSMg3l4IP8Fe`#C|N6mbYJ=n;+}EQm;< z!!N=5j1aAr_uEnnzrEV%_E|JpTb#1p1*}5!Ce!R@d$EtMR~%9# zd;h8=QGT)KMW2IKu_fA_>p_und#-;Q)p%%l0XZOXQicfX8M~7?8}@U^ihu;mizj)t zgV7wk%n-UOb z#!P5q?Ex+*Kx@*p`o$q8FWL*E^$&1*!gpv?Za$YO~{BHeGY*5%4HXUKa_A~~^d z=E*gf6&+LFF^`j4$T~dR)%{I)T?>@Ma?D!gi9I^HqvjPc3-v~=qpX1Mne@*rzT&Xw zQ9DXsSV@PqpEJO-g4A&L{F&;K6W60D!_vs?Vx!?w27XbEuJJP&);)^+VF1nHqHBWu z^>kI$M9yfOY8~|hZ9WB!q-9u&mKhEcRjlf2nm_@s;0D#c|@ED7NZE% zzR;>P5B{o4fzlfsn3CkBK&`OSb-YNrqx@N#4CK!>bQ(V(D#9|l!e9(%sz~PYk@8zt zPN9oK78&-IL_F zhsk1$6p;GqFbtB^ZHHP+cjMvA0(LqlskbdYE_rda>gvQLTiqOQ1~*7lg%z*&p`Ry& zRcG^DbbPj_jOKHTr8uk^15Boj6>hA2S-QY(W-6!FIq8h$<>MI>PYYRenQDBamO#Fv zAH5&ImqKBDn0v5kb|8i0wFhUBJTpT!rB-`zK)^SNnRmLraZcPYK7b{I@+}wXVdW-{Ps17qdRA3JatEd?rPV z4@}(DAMf5EqXCr4-B+~H1P#;t@O}B)tIJ(W6$LrK&0plTmnPpb1TKn3?f?Kk``?D+ zQ!MFqOX7JbsXfQrz`-M@hq7xlfNz;_B{^wbpG8des56x(Q)H)5eLeDwCrVR}hzr~= zM{yXR6IM?kXxauLza#@#u?Y|o;904HCqF<8yT~~c-xyRc0-vxofnxG^(x%>bj5r}N zyFT+xnn-?B`ohA>{+ZZQem=*Xpqz{=j8i2TAC#x-m;;mo{{sLB_z(UoAqD=A#*juZ zCv=J~i*O8;F}A^Wf#+zx;~3B{57xtoxC&j^ie^?**T`WT2OPRtC`xj~+3Kprn=rVM zVJ|h5ux%S{dO}!mq93}P+h36mZ5aZg1-?vhL$ke1d52qIiXSE(llCr5i=QUS?LIjc zV$4q=-)aaR4wsrQv}^shL5u%6;`uiSEs<1nG^?$kl$^6DL z43CjY`M*p}ew}}3rXc7Xck@k41jx}c;NgEIhKZ*jsBRZUP-x2cm;F1<5$jefl|ppO zmZd%%?gMJ^g9=RZ^#8Mf5aWNVhjAS^|DQO+q$)oeob_&ZLFL(zur$)); zU19yRm)z<4&4-M}7!9+^Wl}Uk?`S$#V2%pQ*SIH5KI-mn%i;Z7-)m$mN9CnI$G7?# zo`zVrUwoSL&_dJ92YhX5TKqaRkfPgC4=Q&=K+;_aDs&OU0&{WFH}kKX6uNQC6%oUH z2DZa1s3%Vtk|bglbxep-w)PbFG!J17`<$g8lVhqD2w;Z0zGsh-r zxZ13G$G<48leNqR!DCVt9)@}(zMI5w6Wo=N zpP1*3DI;~h2WDWgcKn*f!+ORD)f$DZFwgKBafEZmeXQMAsq9sxP9A)7zOYnkHT9JU zRA`umgmP9d6=PHmFIgx=0$(sjb>+0CHG)K@cPG{IxaJ&Ueo8)0RWgV9+gO7+Bl1(F z7!BslJ2MP*PWJ;x)QXbR$6jEr5q3 z(3}F@YO_P1NyTdEXRLU6fp?9V2-S=E+YaeLL{Y)W%6`k7$(EW8EZSA*(+;e5@jgD^I zaJQ2|oCM1n!A&-8`;#RDcZyk*+RPkn_r8?Ak@agHiSp*qFNX)&i21HE?yuZ;-C<3C zwJGd1lx5UzViP7sZJ&|LqH*mryb}y|%AOw+v)yc`qM)03qyyrqhX?ub`Cjwx2PrR! z)_z>5*!*$x1=Qa-0uE7jy0z`>|Ni#X+uV|%_81F7)b+nf%iz=`fF4g5UfHS_?PHbr zB;0$bK@=di?f`dS(j{l3-tSCfp~zUuva+=EWxJcRfp(<$@vd(GigM&~vaYZ0c#BTs z3ijkxMl=vw5AS&DcXQ%eeKt!uKvh2l3W?&3=dBHU=Gz?O!40S&&~ei2vg**c$o;i89~6DVns zG>9a*`k5)NI9|?W!@9>rzJ;9EJ=YlJTx1r1BA?H`LWijk(rTax9(OAu;q4_wTj-yj z1%W4GW&K4T=uEGb+E!>W0SD_C0RR91 literal 0 HcmV?d00001 diff --git a/packages/dart_client/example/web/icons/Icon-512.png b/packages/dart_client/example/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 zcmd5=2T+s!lYZ%-(h(2@5fr2dC?F^$C=i-}R6$UX8af(!je;W5yC_|HmujSgN*6?W z3knF*TL1$|?oD*=zPbBVex*RUIKsL<(&Rj9%^UD2IK3W?2j>D?eWQgvS-HLymHo9%~|N2Q{~j za?*X-{b9JRowv_*Mh|;*-kPFn>PI;r<#kFaxFqbn?aq|PduQg=2Q;~Qc}#z)_T%x9 zE|0!a70`58wjREmAH38H1)#gof)U3g9FZ^ zF7&-0^Hy{4XHWLoC*hOG(dg~2g6&?-wqcpf{ z&3=o8vw7lMi22jCG9RQbv8H}`+}9^zSk`nlR8?Z&G2dlDy$4#+WOlg;VHqzuE=fM@ z?OI6HEJH4&tA?FVG}9>jAnq_^tlw8NbjNhfqk2rQr?h(F&WiKy03Sn=-;ZJRh~JrD zbt)zLbnabttEZ>zUiu`N*u4sfQaLE8-WDn@tHp50uD(^r-}UsUUu)`!Rl1PozAc!a z?uj|2QDQ%oV-jxUJmJycySBINSKdX{kDYRS=+`HgR2GO19fg&lZKyBFbbXhQV~v~L za^U944F1_GtuFXtvDdDNDvp<`fqy);>Vw=ncy!NB85Tw{&sT5&Ox%-p%8fTS;OzlRBwErvO+ROe?{%q-Zge=%Up|D4L#>4K@Ke=x%?*^_^P*KD zgXueMiS63!sEw@fNLB-i^F|@Oib+S4bcy{eu&e}Xvb^(mA!=U=Xr3||IpV~3K zQWzEsUeX_qBe6fky#M zzOJm5b+l;~>=sdp%i}}0h zO?B?i*W;Ndn02Y0GUUPxERG`3Bjtj!NroLoYtyVdLtl?SE*CYpf4|_${ku2s`*_)k zN=a}V8_2R5QANlxsq!1BkT6$4>9=-Ix4As@FSS;1q^#TXPrBsw>hJ}$jZ{kUHoP+H zvoYiR39gX}2OHIBYCa~6ERRPJ#V}RIIZakUmuIoLF*{sO8rAUEB9|+A#C|@kw5>u0 zBd=F!4I)Be8ycH*)X1-VPiZ+Ts8_GB;YW&ZFFUo|Sw|x~ZajLsp+_3gv((Q#N>?Jz zFBf`~p_#^${zhPIIJY~yo!7$-xi2LK%3&RkFg}Ax)3+dFCjGgKv^1;lUzQlPo^E{K zmCnrwJ)NuSaJEmueEPO@(_6h3f5mFffhkU9r8A8(JC5eOkux{gPmx_$Uv&|hyj)gN zd>JP8l2U&81@1Hc>#*su2xd{)T`Yw< zN$dSLUN}dfx)Fu`NcY}TuZ)SdviT{JHaiYgP4~@`x{&h*Hd>c3K_To9BnQi@;tuoL z%PYQo&{|IsM)_>BrF1oB~+`2_uZQ48z9!)mtUR zdfKE+b*w8cPu;F6RYJiYyV;PRBbThqHBEu_(U{(gGtjM}Zi$pL8Whx}<JwE3RM0F8x7%!!s)UJVq|TVd#hf1zVLya$;mYp(^oZQ2>=ZXU1c$}f zm|7kfk>=4KoQoQ!2&SOW5|JP1)%#55C$M(u4%SP~tHa&M+=;YsW=v(Old9L3(j)`u z2?#fK&1vtS?G6aOt@E`gZ9*qCmyvc>Ma@Q8^I4y~f3gs7*d=ATlP>1S zyF=k&6p2;7dn^8?+!wZO5r~B+;@KXFEn^&C=6ma1J7Au6y29iMIxd7#iW%=iUzq&C=$aPLa^Q zncia$@TIy6UT@69=nbty5epP>*fVW@5qbUcb2~Gg75dNd{COFLdiz3}kODn^U*=@E z0*$7u7Rl2u)=%fk4m8EK1ctR!6%Ve`e!O20L$0LkM#f+)n9h^dn{n`T*^~d+l*Qlx z$;JC0P9+en2Wlxjwq#z^a6pdnD6fJM!GV7_%8%c)kc5LZs_G^qvw)&J#6WSp< zmsd~1-(GrgjC56Pdf6#!dt^y8Rg}!#UXf)W%~PeU+kU`FeSZHk)%sFv++#Dujk-~m zFHvVJC}UBn2jN& zs!@nZ?e(iyZPNo`p1i#~wsv9l@#Z|ag3JR>0#u1iW9M1RK1iF6-RbJ4KYg?B`dET9 zyR~DjZ>%_vWYm*Z9_+^~hJ_|SNTzBKx=U0l9 z9x(J96b{`R)UVQ$I`wTJ@$_}`)_DyUNOso6=WOmQKI1e`oyYy1C&%AQU<0-`(ow)1 zT}gYdwWdm4wW6|K)LcfMe&psE0XGhMy&xS`@vLi|1#Za{D6l@#D!?nW87wcscUZgELT{Cz**^;Zb~7 z(~WFRO`~!WvyZAW-8v!6n&j*PLm9NlN}BuUN}@E^TX*4Or#dMMF?V9KBeLSiLO4?B zcE3WNIa-H{ThrlCoN=XjOGk1dT=xwwrmt<1a)mrRzg{35`@C!T?&_;Q4Ce=5=>z^*zE_c(0*vWo2_#TD<2)pLXV$FlwP}Ik74IdDQU@yhkCr5h zn5aa>B7PWy5NQ!vf7@p_qtC*{dZ8zLS;JetPkHi>IvPjtJ#ThGQD|Lq#@vE2xdl%`x4A8xOln}BiQ92Po zW;0%A?I5CQ_O`@Ad=`2BLPPbBuPUp@Hb%a_OOI}y{Rwa<#h z5^6M}s7VzE)2&I*33pA>e71d78QpF>sNK;?lj^Kl#wU7G++`N_oL4QPd-iPqBhhs| z(uVM}$ItF-onXuuXO}o$t)emBO3Hjfyil@*+GF;9j?`&67GBM;TGkLHi>@)rkS4Nj zAEk;u)`jc4C$qN6WV2dVd#q}2X6nKt&X*}I@jP%Srs%%DS92lpDY^K*Sx4`l;aql$ zt*-V{U&$DM>pdO?%jt$t=vg5|p+Rw?SPaLW zB6nvZ69$ne4Z(s$3=Rf&RX8L9PWMV*S0@R zuIk&ba#s6sxVZ51^4Kon46X^9`?DC9mEhWB3f+o4#2EXFqy0(UTc>GU| zGCJmI|Dn-dX#7|_6(fT)>&YQ0H&&JX3cTvAq(a@ydM4>5Njnuere{J8p;3?1az60* z$1E7Yyxt^ytULeokgDnRVKQw9vzHg1>X@@jM$n$HBlveIrKP5-GJq%iWH#odVwV6cF^kKX(@#%%uQVb>#T6L^mC@)%SMd4DF? zVky!~ge27>cpUP1Vi}Z32lbLV+CQy+T5Wdmva6Fg^lKb!zrg|HPU=5Qu}k;4GVH+x z%;&pN1LOce0w@9i1Mo-Y|7|z}fbch@BPp2{&R-5{GLoeu8@limQmFF zaJRR|^;kW_nw~0V^ zfTnR!Ni*;-%oSHG1yItARs~uxra|O?YJxBzLjpeE-=~TO3Dn`JL5Gz;F~O1u3|FE- zvK2Vve`ylc`a}G`gpHg58Cqc9fMoy1L}7x7T>%~b&irrNMo?np3`q;d3d;zTK>nrK zOjPS{@&74-fA7j)8uT9~*g23uGnxwIVj9HorzUX#s0pcp2?GH6i}~+kv9fWChtPa_ z@T3m+$0pbjdQw7jcnHn;Pi85hk_u2-1^}c)LNvjdam8K-XJ+KgKQ%!?2n_!#{$H|| zLO=%;hRo6EDmnOBKCL9Cg~ETU##@u^W_5joZ%Et%X_n##%JDOcsO=0VL|Lkk!VdRJ z^|~2pB@PUspT?NOeO?=0Vb+fAGc!j%Ufn-cB`s2A~W{Zj{`wqWq_-w0wr@6VrM zbzni@8c>WS!7c&|ZR$cQ;`niRw{4kG#e z70e!uX8VmP23SuJ*)#(&R=;SxGAvq|&>geL&!5Z7@0Z(No*W561n#u$Uc`f9pD70# z=sKOSK|bF~#khTTn)B28h^a1{;>EaRnHj~>i=Fnr3+Fa4 z`^+O5_itS#7kPd20rq66_wH`%?HNzWk@XFK0n;Z@Cx{kx==2L22zWH$Yg?7 zvDj|u{{+NR3JvUH({;b*$b(U5U z7(lF!1bz2%06+|-v(D?2KgwNw7( zJB#Tz+ZRi&U$i?f34m7>uTzO#+E5cbaiQ&L}UxyOQq~afbNB4EI{E04ZWg53w0A{O%qo=lF8d zf~ktGvIgf-a~zQoWf>loF7pOodrd0a2|BzwwPDV}ShauTK8*fmF6NRbO>Iw9zZU}u zw8Ya}?seBnEGQDmH#XpUUkj}N49tP<2jYwTFp!P+&Fd(%Z#yo80|5@zN(D{_pNow*&4%ql zW~&yp@scb-+Qj-EmErY+Tu=dUmf@*BoXY2&oKT8U?8?s1d}4a`Aq>7SV800m$FE~? zjmz(LY+Xx9sDX$;vU`xgw*jLw7dWOnWWCO8o|;}f>cu0Q&`0I{YudMn;P;L3R-uz# zfns_mZED_IakFBPP2r_S8XM$X)@O-xVKi4`7373Jkd5{2$M#%cRhWer3M(vr{S6>h zj{givZJ3(`yFL@``(afn&~iNx@B1|-qfYiZu?-_&Z8+R~v`d6R-}EX9IVXWO-!hL5 z*k6T#^2zAXdardU3Ao~I)4DGdAv2bx{4nOK`20rJo>rmk3S2ZDu}))8Z1m}CKigf0 z3L`3Y`{huj`xj9@`$xTZzZc3je?n^yG<8sw$`Y%}9mUsjUR%T!?k^(q)6FH6Af^b6 zlPg~IEwg0y;`t9y;#D+uz!oE4VP&Je!<#q*F?m5L5?J3i@!0J6q#eu z!RRU`-)HeqGi_UJZ(n~|PSNsv+Wgl{P-TvaUQ9j?ZCtvb^37U$sFpBrkT{7Jpd?HpIvj2!}RIq zH{9~+gErN2+}J`>Jvng2hwM`=PLNkc7pkjblKW|+Fk9rc)G1R>Ww>RC=r-|!m-u7( zc(a$9NG}w#PjWNMS~)o=i~WA&4L(YIW25@AL9+H9!?3Y}sv#MOdY{bb9j>p`{?O(P zIvb`n?_(gP2w3P#&91JX*md+bBEr%xUHMVqfB;(f?OPtMnAZ#rm5q5mh;a2f_si2_ z3oXWB?{NF(JtkAn6F(O{z@b76OIqMC$&oJ_&S|YbFJ*)3qVX_uNf5b8(!vGX19hsG z(OP>RmZp29KH9Ge2kKjKigUmOe^K_!UXP`von)PR8Qz$%=EmOB9xS(ZxE_tnyzo}7 z=6~$~9k0M~v}`w={AeqF?_)9q{m8K#6M{a&(;u;O41j)I$^T?lx5(zlebpY@NT&#N zR+1bB)-1-xj}R8uwqwf=iP1GbxBjneCC%UrSdSxK1vM^i9;bUkS#iRZw2H>rS<2<$ zNT3|sDH>{tXb=zq7XZi*K?#Zsa1h1{h5!Tq_YbKFm_*=A5-<~j63he;4`77!|LBlo zR^~tR3yxcU=gDFbshyF6>o0bdp$qmHS7D}m3;^QZq9kBBU|9$N-~oU?G5;jyFR7>z hN`IR97YZXIo@y!QgFWddJ3|0`sjFx!m))><{BI=FK%f8s literal 0 HcmV?d00001 diff --git a/packages/dart_client/example/web/index.html b/packages/dart_client/example/web/index.html new file mode 100644 index 00000000..97221146 --- /dev/null +++ b/packages/dart_client/example/web/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + stream_chat + + + + + + diff --git a/packages/dart_client/example/web/manifest.json b/packages/dart_client/example/web/manifest.json new file mode 100644 index 00000000..bad373ca --- /dev/null +++ b/packages/dart_client/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "stream_chat", + "short_name": "stream_chat", + "start_url": ".", + "display": "minimal-ui", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/packages/dart_client/lib/src/api/channel.dart b/packages/dart_client/lib/src/api/channel.dart new file mode 100644 index 00000000..5a0dcbfa --- /dev/null +++ b/packages/dart_client/lib/src/api/channel.dart @@ -0,0 +1,1518 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; + +import 'package:dio/dio.dart'; +import 'package:logging/logging.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:stream_chat/src/api/retry_queue.dart'; +import 'package:stream_chat/src/event_type.dart'; +import 'package:stream_chat/src/models/channel_state.dart'; +import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:uuid/uuid.dart'; + +import '../client.dart'; +import '../models/event.dart'; +import '../models/member.dart'; +import '../models/message.dart'; +import 'requests.dart'; +import 'responses.dart'; + +/// This a the class that manages a specific channel. +class Channel { + /// Create a channel client instance. + Channel( + this._client, + this.type, + this._id, + this._extraData, + ) : _cid = _id != null ? '$type:$_id' : null { + _client.logger.info('New Channel instance not initialized created'); + } + + /// Create a channel client instance from a [ChannelState] object + Channel.fromState(this._client, ChannelState channelState) { + _cid = channelState.channel.cid; + _id = channelState.channel.id; + type = channelState.channel.type; + + state = ChannelClientState(this, channelState); + _initializedCompleter.complete(true); + _startCleaning(); + + _client.logger.info('New Channel instance initialized created'); + } + + /// This client state + ChannelClientState state; + + /// The channel type + String type; + + String _id; + String _cid; + Map _extraData; + + set extraData(Map extraData) { + if (_initializedCompleter.isCompleted) { + throw Exception( + 'Once the channel is initialized you should use channel.update to update channel data'); + } + _extraData = extraData; + } + + /// Returns true if the channel is muted + bool get isMuted => + _client.state.user?.channelMutes + ?.any((element) => element.channel.cid == cid) == + true; + + /// Returns true if the channel is muted as a stream + Stream get isMutedStream => _client.state.userStream?.map((event) => + event.channelMutes?.any((element) => element.channel.cid == cid) == true); + + /// True if the channel is a group + bool get isGroup => memberCount != 2; + + /// True if the channel is distinct + bool get isDistinct => id?.startsWith('!members') == true; + + /// Channel configuration + ChannelConfig get config => state?._channelState?.channel?.config; + + /// Channel configuration as a stream + Stream get configStream => + state?.channelStateStream?.map((cs) => cs.channel?.config); + + /// Channel user creator + User get createdBy => state?._channelState?.channel?.createdBy; + + /// Channel user creator as a stream + Stream get createdByStream => + state?.channelStateStream?.map((cs) => cs.channel?.createdBy); + + /// Channel frozen status + bool get frozen => state?._channelState?.channel?.frozen; + + /// Channel frozen status as a stream + Stream get frozenStream => + state?.channelStateStream?.map((cs) => cs.channel?.frozen); + + /// Channel creation date + DateTime get createdAt => state?._channelState?.channel?.createdAt; + + /// Channel creation date as a stream + Stream get createdAtStream => + state?.channelStateStream?.map((cs) => cs.channel?.createdAt); + + /// Channel last message date + DateTime get lastMessageAt => state?._channelState?.channel?.lastMessageAt; + + /// Channel last message date as a stream + Stream get lastMessageAtStream => + state?.channelStateStream?.map((cs) => cs.channel?.lastMessageAt); + + /// Channel updated date + DateTime get updatedAt => state?._channelState?.channel?.updatedAt; + + /// Channel updated date as a stream + Stream get updatedAtStream => + state?.channelStateStream?.map((cs) => cs.channel?.updatedAt); + + /// Channel deletion date + DateTime get deletedAt => state?._channelState?.channel?.deletedAt; + + /// Channel deletion date as a stream + Stream get deletedAtStream => + state?.channelStateStream?.map((cs) => cs.channel?.deletedAt); + + /// Channel member count + int get memberCount => state?._channelState?.channel?.memberCount; + + /// Channel member count as a stream + Stream get memberCountStream => + state?.channelStateStream?.map((cs) => cs.channel?.memberCount); + + /// Channel id + String get id => state?._channelState?.channel?.id ?? _id; + + /// Channel id as a stream + Stream get idStream => + state?.channelStateStream?.map((cs) => cs.channel?.id ?? _id); + + /// Channel cid + String get cid => state?._channelState?.channel?.cid ?? _cid; + + /// Channel team + String get team => state?._channelState?.channel?.team; + + /// Channel cid as a stream + Stream get cidStream => + state?.channelStateStream?.map((cs) => cs.channel?.cid ?? _cid); + + /// Channel extra data + Map get extraData => + state?._channelState?.channel?.extraData; + + /// Channel extra data as a stream + Stream> get extraDataStream => + state?.channelStateStream?.map((cs) => cs.channel?.extraData); + + /// The main Stream chat client + Client get client => _client; + final Client _client; + + String get _channelURL => '/channels/$type/$id'; + + final Completer _initializedCompleter = Completer(); + + /// True if this is initialized + /// Call [watch] to initialize the client or instantiate it using [Channel.fromState] + Future get initialized => _initializedCompleter.future; + + /// Send a message to this channel + Future sendMessage(Message message) async { + final messageId = message.id ?? Uuid().v4(); + final quotedMessage = state?.messages?.firstWhere( + (m) => m.id == message?.quotedMessageId, + orElse: () => null, + ); + final newMessage = message.copyWith( + createdAt: message.createdAt ?? DateTime.now(), + user: _client.state.user, + id: messageId, + quotedMessage: quotedMessage, + status: MessageSendingStatus.SENDING, + ); + + if (message.parentId != null && message.id == null) { + final parentMessage = + state.messages.firstWhere((m) => m.id == message.parentId); + + state?.addMessage(parentMessage.copyWith( + replyCount: parentMessage.replyCount + 1, + )); + } + + state?.addMessage(newMessage); + + try { + final response = await _client.post( + '$_channelURL/message', + data: { + 'message': message + .copyWith( + id: messageId, + ) + .toJson() + }, + ); + + final res = _client.decode(response.data, SendMessageResponse.fromJson); + + state?.addMessage(res.message); + + return res; + } catch (error) { + if (error is DioError && error.type != DioErrorType.RESPONSE) { + state?.retryQueue?.add([newMessage]); + } + rethrow; + } + } + + /// Send a file to this channel + Future sendFile(MultipartFile file) async { + final response = await _client.post( + '$_channelURL/file', + data: FormData.fromMap({'file': file}), + ); + return _client.decode(response.data, SendFileResponse.fromJson); + } + + /// Send an image to this channel + Future sendImage(MultipartFile file) async { + final response = await _client.post( + '$_channelURL/image', + data: FormData.fromMap({'file': file}), + ); + return _client.decode(response.data, SendImageResponse.fromJson); + } + + /// Delete a file from this channel + Future deleteFile(String url) async { + final response = await _client + .delete('$_channelURL/file', queryParameters: {'url': url}); + return _client.decode(response.data, EmptyResponse.fromJson); + } + + /// Delete an image from this channel + Future deleteImage(String url) async { + final response = await _client + .delete('$_channelURL/image', queryParameters: {'url': url}); + return _client.decode(response.data, EmptyResponse.fromJson); + } + + /// Send an event on this channel + Future sendEvent(Event event) { + _checkInitialized(); + return _client.post( + '$_channelURL/event', + data: {'event': event.toJson()}, + ).then((res) { + return _client.decode(res.data, EmptyResponse.fromJson); + }); + } + + /// Send a reaction to this channel + /// Set [enforceUnique] to true to remove the existing user reaction + Future sendReaction( + Message message, + String type, { + Map extraData = const {}, + bool enforceUnique = false, + }) async { + final messageId = message.id; + final data = Map.from(extraData) + ..addAll({ + 'type': type, + }); + + final res = await _client.post( + '/messages/$messageId/reaction', + data: { + 'reaction': data, + 'enforce_unique': enforceUnique, + }, + ); + return _client.decode(res.data, SendReactionResponse.fromJson); + } + + /// Delete a reaction from this channel + Future deleteReaction(Message message, Reaction reaction) { + _checkInitialized(); + + return client + .delete('/messages/${message.id}/reaction/${reaction.type}') + .then((res) => _client.decode(res.data, EmptyResponse.fromJson)); + } + + /// Edit the channel custom data + Future update( + Map channelData, [ + Message updateMessage, + ]) async { + final response = await _client.post(_channelURL, data: { + if (updateMessage != null) + 'message': updateMessage.copyWith(updatedAt: DateTime.now()).toJson(), + 'data': channelData, + }); + return _client.decode(response.data, UpdateChannelResponse.fromJson); + } + + /// Delete this channel. Messages are permanently removed. + Future delete() async { + final response = await _client.delete(_channelURL); + return _client.decode(response.data, EmptyResponse.fromJson); + } + + /// Removes all messages from the channel + Future truncate() async { + final response = await _client.post('$_channelURL/truncate'); + return _client.decode(response.data, EmptyResponse.fromJson); + } + + /// Accept invitation to the channel + Future acceptInvite([Message message]) async { + final res = await _client.post(_channelURL, + data: {'accept_invite': true, 'message': message?.toJson()}); + return _client.decode(res.data, AcceptInviteResponse.fromJson); + } + + /// Reject invitation to the channel + Future rejectInvite([Message message]) async { + final res = await _client.post(_channelURL, + data: {'reject_invite': true, 'message': message?.toJson()}); + return _client.decode(res.data, RejectInviteResponse.fromJson); + } + + /// Add members to the channel + Future addMembers( + List memberIds, [ + Message message, + ]) async { + final res = await _client.post(_channelURL, data: { + 'add_members': memberIds, + 'message': message?.toJson(), + }); + return _client.decode(res.data, AddMembersResponse.fromJson); + } + + /// Invite members to the channel + Future inviteMembers( + List memberIds, [ + Message message, + ]) async { + final res = await _client.post(_channelURL, data: { + 'invites': memberIds, + 'message': message?.toJson(), + }); + return _client.decode(res.data, InviteMembersResponse.fromJson); + } + + /// Remove members from the channel + Future removeMembers( + List memberIds, [ + Message message, + ]) async { + final res = await _client.post(_channelURL, data: { + 'remove_members': memberIds, + 'message': message?.toJson(), + }); + return _client.decode(res.data, RemoveMembersResponse.fromJson); + } + + /// Send action for a specific message of this channel + Future sendAction( + Message message, + Map formData, + ) async { + _checkInitialized(); + + final messageId = message.id; + final response = await _client.post('/messages/$messageId/action', data: { + 'id': id, + 'type': type, + 'form_data': formData, + 'message_id': messageId, + }); + + final res = _client.decode(response.data, SendActionResponse.fromJson); + + if (res.message != null) { + state.addMessage(res.message); + } else { + final oldIndex = state.messages?.indexWhere((m) => m.id == messageId); + + Message oldMessage; + if (oldIndex != null && oldIndex != -1) { + oldMessage = state.messages[oldIndex]; + state.updateChannelState(state._channelState.copyWith( + messages: state.messages..remove(oldMessage), + )); + } else { + oldMessage = state.threads.values + .expand((messages) => messages) + .firstWhere((m) => m.id == messageId, orElse: () => null); + if (oldMessage?.parentId != null) { + final parentMessage = state.messages.firstWhere( + (element) => element.id == oldMessage.parentId, + orElse: () => null, + ); + if (parentMessage != null) { + state.addMessage(parentMessage.copyWith( + replyCount: parentMessage.replyCount - 1)); + } + state.updateThreadInfo(oldMessage.parentId, + state.threads[oldMessage.parentId]..remove(oldMessage)); + } + } + + await _client.offlineStorage?.deleteMessages([messageId]); + } + + return res; + } + + /// Mark all channel messages as read + Future markRead() async { + _checkInitialized(); + state._unreadCountController.add(0); + final response = await _client.post('$_channelURL/read', data: {}); + return _client.decode(response.data, EmptyResponse.fromJson); + } + + /// Loads the initial channel state and watches for changes + Future watch([Map options = const {}]) async { + final watchOptions = Map.from({ + 'state': true, + 'watch': true, + 'presence': false, + }) + ..addAll(options); + + var response; + + try { + response = await query(options: watchOptions); + } catch (error, stackTrace) { + if (!_initializedCompleter.isCompleted) { + _initializedCompleter.completeError(error, stackTrace); + } + rethrow; + } + + if (state == null) { + _initState(response); + } + + return response; + } + + void _initState(ChannelState channelState) { + state = ChannelClientState(this, channelState); + client.state.channels[cid] = this; + if (!_initializedCompleter.isCompleted) { + _initializedCompleter.complete(true); + } + _startCleaning(); + } + + /// Stop watching the channel + Future stopWatching() async { + final response = await _client.post( + '$_channelURL/stop-watching', + data: {}, + ); + return _client.decode(response?.data, EmptyResponse.fromJson); + } + + /// List the message replies for a parent message + /// Set [preferOffline] to true to avoid the api call if the data is already in the offline storage + Future getReplies( + String parentId, + PaginationParams options, { + bool preferOffline = false, + }) async { + final cachedReplies = await _client.offlineStorage?.getReplies( + parentId, + lessThan: options?.lessThan, + ); + if (cachedReplies != null && cachedReplies.isNotEmpty) { + state?.updateThreadInfo(parentId, cachedReplies); + if (preferOffline) { + return QueryRepliesResponse()..messages = cachedReplies; + } + } + + final response = await _client.get('/messages/$parentId/replies', + queryParameters: options.toJson()); + + final repliesResponse = _client.decode( + response.data, + QueryRepliesResponse.fromJson, + ); + + state?.updateThreadInfo(parentId, repliesResponse.messages); + + return repliesResponse; + } + + /// List the reactions for a message in the channel + Future getReactions( + String messageID, + PaginationParams options, + ) async { + final response = await _client.get( + '/messages/$messageID/reactions', + queryParameters: options.toJson(), + ); + return _client.decode( + response.data, QueryReactionsResponse.fromJson); + } + + /// Retrieves a list of messages by ID + Future getMessagesById( + List messageIDs) async { + final response = await _client.get( + '$_channelURL/messages', + queryParameters: {'ids': messageIDs.join(',')}, + ); + + final res = _client.decode( + response.data, + GetMessagesByIdResponse.fromJson, + ); + + state?.updateChannelState(ChannelState(messages: res.messages)); + + return res; + } + + /// Retrieves a list of messages by ID + Future translateMessage( + String messageId, + String language, + ) async { + final response = await _client.post( + '/messages/$messageId/translate', + data: { + 'language': language, + }, + ); + return _client.decode( + response.data, + TranslateMessageResponse.fromJson, + ); + } + + /// Creates a new channel + Future create() async { + return query(options: { + 'watch': false, + 'state': false, + 'presence': false, + }); + } + + /// Query the API, get messages, members or other channel fields + /// Set [preferOffline] to true to avoid the api call if the data is already in the offline storage + Future query({ + Map options = const {}, + PaginationParams messagesPagination, + PaginationParams membersPagination, + PaginationParams watchersPagination, + bool preferOffline = false, + }) async { + var path = '/channels/$type'; + if (id != null) { + path = '$path/$id'; + } + path = '$path/query'; + + final payload = Map.from({ + 'state': true, + }) + ..addAll(options); + + if (_extraData != null) { + payload['data'] = _extraData; + } + + if (messagesPagination != null) { + payload['messages'] = messagesPagination.toJson(); + } + if (membersPagination != null) { + payload['members'] = membersPagination.toJson(); + } + if (watchersPagination != null) { + payload['watchers'] = watchersPagination.toJson(); + } + + if (preferOffline && cid != null) { + final updatedState = await _client.offlineStorage?.getChannel( + cid, + limit: messagesPagination?.limit, + messageLessThan: messagesPagination?.lessThan, + messageGreaterThan: messagesPagination?.greaterThan, + ); + if (updatedState != null && updatedState.messages.isNotEmpty) { + if (state == null) { + _initState(updatedState); + } else { + state?.updateChannelState(updatedState); + } + return updatedState; + } + } + + final response = await _client.post(path, data: payload); + final updatedState = _client.decode(response.data, ChannelState.fromJson); + + if (_id == null) { + _id = updatedState.channel.id; + _cid = updatedState.channel.cid; + } + + state?.updateChannelState(updatedState); + return updatedState; + } + + /// Query channel members + Future queryMembers({ + Map filter, + List sort, + PaginationParams pagination, + }) async { + final payload = { + 'sort': sort, + 'filter_conditions': filter, + 'type': type, + }; + + if (pagination != null) { + payload.addAll(pagination.toJson()); + } + + if (id != null) { + payload['id'] = id; + } else if (state?.members?.isNotEmpty == true) { + payload['members'] = state.members; + } + + final rawRes = await _client.get('/members', queryParameters: { + 'payload': jsonEncode(payload), + }); + final response = _client.decode(rawRes.data, QueryMembersResponse.fromJson); + return response; + } + + /// Mutes the channel + Future mute({Duration expiration}) async { + final response = await _client.post('/moderation/mute/channel', data: { + 'channel_cid': cid, + if (expiration != null) 'expiration': expiration.inMilliseconds, + }); + return _client.decode(response.data, EmptyResponse.fromJson); + } + + /// Unmutes the channel + Future unmute() async { + final response = await _client.post('/moderation/unmute/channel', data: { + 'channel_cid': cid, + }); + return _client.decode(response.data, EmptyResponse.fromJson); + } + + /// Bans a user from the channel + Future banUser( + String userID, + Map options, + ) async { + _checkInitialized(); + final opts = Map.from(options) + ..addAll({ + 'type': type, + 'id': id, + }); + return _client.banUser(userID, opts); + } + + /// Remove the ban for a user in the channel + Future unbanUser(String userID) async { + _checkInitialized(); + return _client.unbanUser(userID, { + 'type': type, + 'id': id, + }); + } + + /// Shadow bans a user from the channel + Future shadowBan( + String userID, + Map options, + ) async { + _checkInitialized(); + final opts = Map.from(options) + ..addAll({ + 'type': type, + 'id': id, + }); + return _client.shadowBan(userID, opts); + } + + /// Remove the shadow ban for a user in the channel + Future removeShadowBan(String userID) async { + _checkInitialized(); + return _client.removeShadowBan(userID, { + 'type': type, + 'id': id, + }); + } + + /// Hides the channel from [Client.queryChannels] for the user until a message is added + /// If [clearHistory] is set to true - all messages will be removed for the user + Future hide({bool clearHistory = false}) async { + _checkInitialized(); + final response = await _client + .post('$_channelURL/hide', data: {'clear_history': clearHistory}); + + if (clearHistory == true) { + state.truncate(); + await _client.offlineStorage?.deleteChannelsMessages([_cid]); + } + + return _client.decode(response.data, EmptyResponse.fromJson); + } + + /// Removes the hidden status for the channel + Future show() async { + _checkInitialized(); + final response = await _client.post('$_channelURL/show'); + return _client.decode(response.data, EmptyResponse.fromJson); + } + + /// Stream of [Event] coming from websocket connection specific for the channel + /// Pass an eventType as parameter in order to filter just a type of event + Stream on([ + String eventType, + String eventType2, + String eventType3, + String eventType4, + ]) { + return _client + .on( + eventType, + eventType2, + eventType3, + eventType4, + ) + .where((e) => e.cid == cid); + } + + DateTime _lastTypingEvent; + + /// First of the [EventType.typingStart] and [EventType.typingStop] events based on the users keystrokes. + /// Call this on every keystroke. + Future keyStroke([String parentId]) async { + if (config?.typingEvents == false) { + return; + } + + client.logger.info('start typing'); + final now = DateTime.now(); + + if (_lastTypingEvent == null || + now.difference(_lastTypingEvent).inSeconds >= 2) { + _lastTypingEvent = now; + await sendEvent(Event( + type: EventType.typingStart, + parentId: parentId, + )); + } + } + + /// Sets last typing to null and sends the typing.stop event + Future stopTyping([String parentId]) async { + if (config?.typingEvents == false) { + return; + } + + client.logger.info('stop typing'); + _lastTypingEvent = null; + await sendEvent(Event( + type: EventType.typingStop, + parentId: parentId, + )); + } + + Timer _cleaningTimer; + + void _startCleaning() { + if (config?.typingEvents == false) { + return; + } + + _cleaningTimer = Timer.periodic(Duration(milliseconds: 500), (_) { + final now = DateTime.now(); + + if (_lastTypingEvent != null && + now.difference(_lastTypingEvent).inSeconds > 1) { + stopTyping(); + } + + state._clean(); + }); + } + + /// Call this method to dispose the channel client + void dispose() { + _cleaningTimer.cancel(); + state.dispose(); + } + + void _checkInitialized() { + if (!_initializedCompleter.isCompleted) { + throw Exception( + "Channel $cid hasn't been initialized yet. Make sure to call .watch() or to instantiate the client using [Channel.fromState]"); + } + } +} + +/// The class that handles the state of the channel listening to the events +class ChannelClientState { + final _subscriptions = []; + + /// Creates a new instance listening to events and updating the state + ChannelClientState(this._channel, ChannelState channelState) { + retryQueue = RetryQueue( + channel: _channel, + logger: Logger('RETRY QUEUE ${_channel.cid}'), + ); + + _checkExpiredAttachmentMessages(channelState); + + _channelStateController = BehaviorSubject.seeded(channelState); + + _listenTypingEvents(); + + _listenMessageNew(); + + _listenMessageDeleted(); + + _listenMessageUpdated(); + + _listenReactions(); + + _listenReactionDeleted(); + + _listenReadEvents(); + + _listenChannelTruncated(); + + _listenChannelUpdated(); + + _listenMemberAdded(); + + _listenMemberRemoved(); + + _computeInitialUnread(); + + _channel._client.offlineStorage + ?.getChannelThreads(_channel.cid) + ?.then((threads) { + _threads = threads; + retryFailedMessages(); + }); + } + + void _computeInitialUnread() { + final userRead = channelState?.read?.firstWhere( + (r) => r.user.id == _channel._client.state?.user?.id, + orElse: () => null, + ); + if (userRead != null) { + _unreadCountController.add(userRead.unreadMessages ?? 0); + } + } + + void _checkExpiredAttachmentMessages(ChannelState channelState) { + final expiredAttachmentMessagesId = channelState.messages + ?.where((m) => + !_updatedMessagesIds.contains(m.id) && + m.attachments?.isNotEmpty == true && + m.attachments?.any((e) { + final url = e.imageUrl ?? e.assetUrl; + if (url == null || !url.contains('stream-io-cdn.com')) { + return false; + } + final expiration = + DateTime.parse(Uri.parse(url).queryParameters['Expires']); + return expiration.isBefore(DateTime.now()); + }) == + true) + ?.map((e) => e.id) + ?.toList(); + if (expiredAttachmentMessagesId?.isNotEmpty == true) { + _channel.getMessagesById(expiredAttachmentMessagesId); + _updatedMessagesIds.addAll(expiredAttachmentMessagesId); + } + } + + void _listenMemberAdded() { + _subscriptions.add(_channel.on(EventType.memberAdded).listen((Event e) { + final member = e.member; + updateChannelState(channelState.copyWith( + members: [ + ...channelState.members, + member, + ], + )); + })); + } + + void _listenMemberRemoved() { + _subscriptions.add(_channel.on(EventType.memberRemoved).listen((Event e) { + final user = e.user; + updateChannelState(channelState.copyWith( + members: List.from( + channelState.members..removeWhere((m) => m.userId == user.id)), + )); + })); + } + + void _listenChannelUpdated() { + _subscriptions.add(_channel.on(EventType.channelUpdated).listen((Event e) { + final channel = e.channel; + updateChannelState(channelState.copyWith( + channel: channel, + members: channel.members, + )); + })); + } + + void _listenChannelTruncated() { + _subscriptions.add(_channel + .on(EventType.channelTruncated, EventType.notificationChannelTruncated) + .listen((event) async { + final channel = event.channel; + await _channel._client.offlineStorage + ?.deleteChannelsMessages([channel.cid]); + truncate(); + })); + } + + /// Flag which indicates if [ChannelClientState] contain latest/recent messages or not. + /// This flag should be managed by UI sdks. + /// When false, any new message (received by WebSocket event - [EventType.messageNew]) will not + /// be pushed on to message list. + bool get isUpToDate => _isUpToDateController.value; + + set isUpToDate(bool isUpToDate) => _isUpToDateController.add(isUpToDate); + + /// [isUpToDate] flag count as a stream + Stream get isUpToDateStream => _isUpToDateController.stream; + + final BehaviorSubject _isUpToDateController = + BehaviorSubject.seeded(true); + + /// The retry queue associated to this channel + RetryQueue retryQueue; + + /// Retry failed message + Future retryFailedMessages() async { + final failedMessages = + [...messages, ...threads.values.expand((v) => v)] + .where((message) => + message.status != null && + message.status != MessageSendingStatus.SENT && + message.createdAt.isBefore(DateTime.now().subtract(Duration( + seconds: 1, + )))) + .toList(); + + retryQueue.add(failedMessages); + } + + void _listenReactionDeleted() { + _subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) { + final reaction = event.reaction; + final message = event.message; + _removeMessageReaction(message, reaction); + })); + } + + void _removeMessageReaction(Message message, Reaction reaction) { + if (message.parentId == null || message.showInChannel == true) { + _channelState = _channelState.copyWith( + messages: _channelState?.messages?.map((m) { + if (m.id == message.id) { + return _removeReactionFromMessage(m, reaction); + } + return m; + })?.toList(), + ); + } + + if (message.parentId != null) { + final newThreads = threads; + if (newThreads.containsKey(message.parentId)) { + newThreads[message.parentId] = newThreads[message.parentId].map((m) { + if (m.id == message.id) { + return _removeReactionFromMessage(m, reaction); + } + return m; + }).toList(); + _threads = newThreads; + } + } + } + + void _listenReactions() { + _subscriptions.add(_channel + .on( + EventType.reactionNew, + ) + .listen((event) { + final message = event.message; + _addMessageReaction(message, event.reaction); + })); + } + + void _addMessageReaction(Message message, Reaction reaction) { + if (message.parentId == null || message.showInChannel == true) { + _channelState = _channelState.copyWith( + messages: _channelState.messages.map((m) { + if (message.id == m.id) { + return _addReactionToMessage(m, reaction); + } + return m; + }).toList(), + ); + } + + if (message.parentId != null) { + final newThreads = threads; + if (newThreads.containsKey(message.parentId)) { + newThreads[message.parentId] = newThreads[message.parentId].map((m) { + if (message.id == m.id) { + return _addReactionToMessage(m, reaction); + } + return m; + }).toList(); + _threads = newThreads; + } + } + } + + void _listenMessageUpdated() { + _subscriptions.add(_channel + .on( + EventType.messageUpdated, + EventType.reactionUpdated, + ) + .listen((event) { + final message = event.message; + addMessage(message.copyWith( + ownReactions: message.latestReactions + .where( + (element) => element.user?.id == _channel._client.state.user.id) + .toList(), + )); + })); + } + + void _listenMessageDeleted() { + _subscriptions.add(_channel.on(EventType.messageDeleted).listen((event) { + final message = event.message; + addMessage(message); + })); + } + + void _listenMessageNew() { + _subscriptions.add(_channel + .on( + EventType.messageNew, + EventType.notificationMessageNew, + ) + .listen((event) { + final message = event.message; + if (isUpToDate || + (message.parentId != null && message.showInChannel != true)) { + addMessage(message); + } + + if (_countMessageAsUnread(message)) { + _unreadCountController.add(_unreadCountController.value + 1); + } + })); + } + + /// Add a message to this channel + void addMessage(Message message) { + if (message.parentId == null || message.showInChannel == true) { + final newMessages = List.from(_channelState.messages); + + final oldIndex = newMessages.indexWhere((m) => m.id == message.id); + if (oldIndex != -1) { + newMessages[oldIndex] = newMessages[oldIndex].copyWith( + text: message.text, + type: message.type, + attachments: message.attachments, + mentionedUsers: message.mentionedUsers, + reactionCounts: message.reactionCounts, + reactionScores: message.reactionScores, + latestReactions: message.latestReactions, + ownReactions: message.ownReactions, + parentId: message.parentId, + quotedMessageId: message.quotedMessageId, + quotedMessage: message.quotedMessage, + replyCount: message.replyCount, + showInChannel: message.showInChannel, + command: message.command, + silent: message.silent, + createdAt: message.createdAt, + updatedAt: message.updatedAt, + deletedAt: message.deletedAt, + user: message.user, + status: message.status, + threadParticipants: message.threadParticipants, + extraData: message.extraData, + ); + } else { + newMessages.add(message); + } + + _channelState = _channelState.copyWith( + messages: newMessages, + channel: _channelState.channel.copyWith( + lastMessageAt: message.createdAt, + ), + ); + } + + if (message.parentId != null) { + updateThreadInfo(message.parentId, [message]); + } + } + + void _listenReadEvents() { + if (_channel.config?.readEvents == false) { + return; + } + + _subscriptions.add(_channel + .on( + EventType.messageRead, + EventType.notificationMarkRead, + ) + .listen((event) { + final readList = List.from(_channelState?.read ?? []); + final userReadIndex = read?.indexWhere((r) => r.user.id == event.user.id); + + if (userReadIndex != null && userReadIndex != -1) { + final userRead = readList.removeAt(userReadIndex); + if (userRead.user?.id == _channel._client.state.user.id) { + _unreadCountController.add(0); + } + readList.add(Read( + user: event.user, + lastRead: event.createdAt, + )); + _channelState = _channelState.copyWith(read: readList); + } + })); + } + + Message _addReactionToMessage(Message message, Reaction reaction) { + final newMessage = message.copyWith( + latestReactions: message.latestReactions..add(reaction), + reactionCounts: { + ...message.reactionCounts ?? {}, + reaction.type: (message.reactionCounts == null + ? 0 + : message.reactionCounts[reaction.type] ?? 0) + + 1, + }, + reactionScores: { + ...message.reactionScores ?? {}, + reaction.type: (message.reactionScores == null + ? 0 + : message.reactionScores[reaction.type] ?? 0) + + reaction.score, + }, + ); + + if (reaction.user.id == _channel.client.state.user.id) { + return newMessage.copyWith( + ownReactions: message.ownReactions..add(reaction), + ); + } + + return newMessage; + } + + Message _removeReactionFromMessage(Message message, Reaction reaction) { + final newMessage = message.copyWith( + latestReactions: message.latestReactions + ..removeWhere( + (r) => r.type == reaction.type && r.userId == reaction.userId), + reactionCounts: { + ...message.reactionCounts, + reaction.type: (message.reactionCounts[reaction.type] ?? 0) - 1, + }, + reactionScores: { + ...message.reactionScores ?? {}, + reaction.type: max( + (message.reactionScores == null + ? 0 + : message.reactionScores[reaction.type] ?? 0) - + reaction.score, + 0), + }, + ); + + newMessage.reactionCounts.removeWhere((_, v) => v <= 0); + + if (reaction.user.id == _channel.client.state.user.id) { + return newMessage.copyWith( + ownReactions: message.ownReactions + ..removeWhere((r) => r.type == reaction.type), + ); + } + + return newMessage; + } + + /// Channel message list + List get messages => _channelState.messages; + + /// Channel message list as a stream + Stream> get messagesStream => + channelStateStream.map((cs) => cs.messages); + + /// Get channel last message + Message get lastMessage => _channelState.messages?.isNotEmpty == true + ? _channelState.messages.last + : null; + + /// Get channel last message + Stream get lastMessageStream => messagesStream + .map((event) => event?.isNotEmpty == true ? event.last : null); + + /// Channel members list + List get members => _channelState.members + .map((e) => e.copyWith(user: _channel.client.state.users[e.user.id])) + .toList(); + + /// Channel members list as a stream + Stream> get membersStream => CombineLatestStream.combine2< + List, Map, List>( + channelStateStream.map((cs) => cs.members), + _channel.client.state.usersStream, + (members, users) { + return members + .map((e) => e.copyWith(user: users[e.user.id])) + .toList(); + }, + ); + + /// Channel watcher count + int get watcherCount => _channelState.watcherCount; + + /// Channel watcher count as a stream + Stream get watcherCountStream => + channelStateStream.map((cs) => cs.watcherCount); + + /// Channel watchers list + List get watchers => _channelState.watchers + .map((e) => _channel.client.state.users[e.id] ?? e) + .toList(); + + /// Channel watchers list as a stream + Stream> get watchersStream => + CombineLatestStream.combine2, Map, List>( + channelStateStream.map((cs) => cs.watchers), + _channel.client.state.usersStream, + (watchers, users) { + return watchers.map((e) => users[e.id] ?? e).toList(); + }, + ); + + /// Channel read list + List get read => _channelState.read; + + /// Channel read list as a stream + Stream> get readStream => channelStateStream.map((cs) => cs.read); + + final BehaviorSubject _unreadCountController = BehaviorSubject.seeded(0); + + /// Unread count getter as a stream + Stream get unreadCountStream => _unreadCountController.stream; + + /// Unread count getter + int get unreadCount => _unreadCountController.value; + + bool _countMessageAsUnread(Message message) { + final userId = _channel.client.state?.user?.id; + final userIsMuted = _channel.client.state.user.mutes.firstWhere( + (m) => m.user?.id == message.user.id, + orElse: () => null, + ) != + null; + return message.silent != true && + message.shadowed != true && + message.user.id != userId && + !userIsMuted; + } + + /// Update threads with updated information about messages + void updateThreadInfo(String parentId, List messages) { + final newThreads = Map>.from(threads); + + if (newThreads.containsKey(parentId)) { + newThreads[parentId] = [ + ...newThreads[parentId] + ?.where( + (newMessage) => !messages.any((m) => m.id == newMessage.id)) + ?.toList() ?? + [], + ...messages, + ]; + + newThreads[parentId].sort(_sortByCreatedAt); + } else { + newThreads[parentId] = messages; + } + + _threads = newThreads; + } + + /// Delete all channel messages + void truncate() { + _channelState = _channelState.copyWith( + messages: [], + ); + } + + final List _updatedMessagesIds = []; + + /// Update channelState with updated information + void updateChannelState(ChannelState updatedState) { + final newMessages = [ + ...updatedState?.messages ?? [], + ..._channelState?.messages + ?.where((m) => + updatedState.messages + ?.any((newMessage) => newMessage.id == m.id) != + true) + ?.toList() ?? + [], + ]; + + newMessages.sort(_sortByCreatedAt); + + final newWatchers = [ + ...updatedState?.watchers ?? [], + ..._channelState?.watchers + ?.where((w) => + updatedState.watchers + ?.any((newWatcher) => newWatcher.id == w.id) != + true) + ?.toList() ?? + [], + ]; + + final newMembers = [ + ...updatedState?.members ?? [], + ]; + + final newReads = [ + ...updatedState?.read ?? [], + ..._channelState?.read + ?.where((r) => + updatedState.read + ?.any((newRead) => newRead.user.id == r.user.id) != + true) + ?.toList() ?? + [], + ]; + + _checkExpiredAttachmentMessages(updatedState); + + _channelState = _channelState.copyWith( + messages: newMessages, + channel: _channelState.channel?.copyWith( + lastMessageAt: updatedState.channel?.lastMessageAt, + createdAt: updatedState.channel?.createdAt, + type: updatedState.channel?.type, + extraData: updatedState.channel?.extraData, + updatedAt: updatedState.channel?.updatedAt, + id: updatedState.channel?.id, + createdBy: updatedState.channel?.createdBy, + config: updatedState.channel?.config, + deletedAt: updatedState.channel?.deletedAt, + cid: updatedState.channel?.cid, + frozen: updatedState.channel?.frozen, + memberCount: updatedState.channel?.memberCount, + ), + watchers: newWatchers, + watcherCount: updatedState.watcherCount, + members: newMembers, + read: newReads, + ); + } + + int _sortByCreatedAt(a, b) { + if (a.createdAt == null) { + return 1; + } + + if (b.createdAt == null) { + return -1; + } + + return a.createdAt.compareTo(b.createdAt); + } + + /// The channel state related to this client + ChannelState get _channelState => _channelStateController.value; + + /// The channel state related to this client as a stream + Stream get channelStateStream => _channelStateController.stream; + + /// The channel state related to this client + ChannelState get channelState => _channelStateController.value; + BehaviorSubject _channelStateController; + + set _channelState(ChannelState v) { + _channelStateController.add(v); + _channel._client.offlineStorage?.updateChannelState(v); + } + + /// The channel threads related to this channel + Map> get threads => _threadsController.value; + + /// The channel threads related to this channel as a stream + Stream>> get threadsStream => + _threadsController.stream; + final BehaviorSubject>> _threadsController = + BehaviorSubject.seeded({}); + + set _threads(Map> v) { + _channel._client.offlineStorage?.updateMessages( + v.values.expand((v) => v).toList(), + _channel.cid, + ); + _threadsController.add(v); + } + + /// Channel related typing users last value + List get typingEvents => _typingEventsController.value; + + /// Channel related typing users stream + Stream> get typingEventsStream => _typingEventsController.stream; + final BehaviorSubject> _typingEventsController = + BehaviorSubject.seeded([]); + + final Channel _channel; + final Map _typings = {}; + + void _listenTypingEvents() { + if (_channel.config?.typingEvents == false) { + return; + } + + _subscriptions.add(_channel.on(EventType.typingStart).listen((event) { + if (event.user.id != _channel.client.state.user.id) { + _typings[event.user] = DateTime.now(); + _typingEventsController.add(_typings.keys.toList()); + } + })); + + _subscriptions.add(_channel.on(EventType.typingStop).listen((event) { + if (event.user.id != _channel.client.state.user.id) { + _typings.remove(event.user); + _typingEventsController.add(_typings.keys.toList()); + } + })); + } + + void _clean() { + final now = DateTime.now(); + _typings.forEach((user, lastTypingEvent) { + if (now.difference(lastTypingEvent).inSeconds > 7) { + _channel.client.handleEvent( + Event( + type: EventType.typingStop, + user: user, + cid: _channel.cid, + ), + ); + } + }); + _typingEventsController.add(_typings.keys.toList()); + } + + /// Call this method to dispose this object + void dispose() { + _unreadCountController.close(); + retryQueue.dispose(); + _subscriptions.forEach((s) => s.cancel()); + _channelStateController.close(); + _isUpToDateController.close(); + _threadsController.close(); + _typingEventsController.close(); + } +} diff --git a/packages/dart_client/lib/src/api/connection_status.dart b/packages/dart_client/lib/src/api/connection_status.dart new file mode 100644 index 00000000..28a8e8d4 --- /dev/null +++ b/packages/dart_client/lib/src/api/connection_status.dart @@ -0,0 +1,11 @@ +/// Used to notify the WS connection status +enum ConnectionStatus { + /// WS is connected and everything is good + connected, + + /// WS is connecting (usually reconnecting) + connecting, + + /// WS is disconnected and it's not reconnecting + disconnected, +} diff --git a/packages/dart_client/lib/src/api/requests.dart b/packages/dart_client/lib/src/api/requests.dart new file mode 100644 index 00000000..fdf1f34f --- /dev/null +++ b/packages/dart_client/lib/src/api/requests.dart @@ -0,0 +1,97 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'requests.g.dart'; + +/// Sorting options +@JsonSerializable(createFactory: false) +class SortOption { + /// Ascending order + static const ASC = 1; + + /// Descending order + static const DESC = -1; + + /// A sorting field name + final String field; + + /// A sorting direction + final int direction; + + /// Creates a new SortOption instance + /// + /// For example: + /// ```dart + /// // Sort channels by the last message date: + /// final sorting = SortOption("last_message_at") + /// ``` + const SortOption(this.field, {this.direction = DESC}); + + /// Serialize model to json + Map toJson() => _$SortOptionToJson(this); +} + +/// Pagination options. +@JsonSerializable(createFactory: false, includeIfNull: false) +class PaginationParams { + /// The amount of items requested from the APIs. + final int limit; + + /// The offset of requesting items. + final int offset; + + /// Filter on ids greater than the given value. + @JsonKey(name: 'id_gt') + final String greaterThan; + + /// Filter on ids greater than or equal to the given value. + @JsonKey(name: 'id_gte') + final String greaterThanOrEqual; + + /// Filter on ids smaller than the given value. + @JsonKey(name: 'id_lt') + final String lessThan; + + /// Filter on ids smaller than or equal to the given value. + @JsonKey(name: 'id_lte') + final String lessThanOrEqual; + + /// Creates a new PaginationParams instance + /// + /// For example: + /// ```dart + /// // limit to 50 + /// final paginationParams = PaginationParams(limit: 50); + /// + /// // limit to 50 with offset + /// final paginationParams = PaginationParams(limit: 50, offset: 50); + /// ``` + const PaginationParams({ + this.limit = 10, + this.offset, + this.greaterThan, + this.greaterThanOrEqual, + this.lessThan, + this.lessThanOrEqual, + }); + + /// Serialize model to json + Map toJson() => _$PaginationParamsToJson(this); + + /// Creates a copy of [PaginationParams] with specified attributes overridden. + PaginationParams copyWith({ + int limit, + int offset, + String greaterThan, + String greaterThanOrEqual, + String lessThan, + String lessThanOrEqual, + }) => + PaginationParams( + limit: limit ?? this.limit, + offset: offset ?? this.offset, + greaterThan: greaterThan ?? this.greaterThan, + greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual, + lessThan: lessThan ?? this.lessThan, + lessThanOrEqual: lessThanOrEqual ?? this.lessThanOrEqual, + ); +} diff --git a/packages/dart_client/lib/src/api/requests.g.dart b/packages/dart_client/lib/src/api/requests.g.dart new file mode 100644 index 00000000..ef790776 --- /dev/null +++ b/packages/dart_client/lib/src/api/requests.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'requests.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Map _$SortOptionToJson(SortOption instance) => + { + 'field': instance.field, + 'direction': instance.direction, + }; + +Map _$PaginationParamsToJson(PaginationParams instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('limit', instance.limit); + writeNotNull('offset', instance.offset); + writeNotNull('id_gt', instance.greaterThan); + writeNotNull('id_gte', instance.greaterThanOrEqual); + writeNotNull('id_lt', instance.lessThan); + writeNotNull('id_lte', instance.lessThanOrEqual); + return val; +} diff --git a/packages/dart_client/lib/src/api/responses.dart b/packages/dart_client/lib/src/api/responses.dart new file mode 100644 index 00000000..919a875b --- /dev/null +++ b/packages/dart_client/lib/src/api/responses.dart @@ -0,0 +1,375 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:stream_chat/src/client.dart'; +import 'package:stream_chat/src/models/device.dart'; +import 'package:stream_chat/src/models/event.dart'; + +import '../models/channel_model.dart'; +import '../models/channel_state.dart'; +import '../models/member.dart'; +import '../models/message.dart'; +import '../models/reaction.dart'; +import '../models/read.dart'; +import '../models/user.dart'; + +part 'responses.g.dart'; + +class _BaseResponse { + String duration; +} + +/// Model response for [Client.resync] api call +@JsonSerializable(createToJson: false) +class SyncResponse extends _BaseResponse { + /// The list of events + List events; + + /// Create a new instance from a json + static SyncResponse fromJson(Map json) => + _$SyncResponseFromJson(json); +} + +/// Model response for [Client.queryChannels] api call +@JsonSerializable(createToJson: false) +class QueryChannelsResponse extends _BaseResponse { + /// List of channels state returned by the query + List channels; + + /// Create a new instance from a json + static QueryChannelsResponse fromJson(Map json) => + _$QueryChannelsResponseFromJson(json); +} + +/// Model response for [Client.queryChannels] api call +@JsonSerializable(createToJson: false) +class TranslateMessageResponse extends _BaseResponse { + /// List of channels state returned by the query + TranslatedMessage message; + + /// Create a new instance from a json + static TranslateMessageResponse fromJson(Map json) => + _$TranslateMessageResponseFromJson(json); +} + +/// Model response for [Client.queryChannels] api call +@JsonSerializable(createToJson: false) +class QueryMembersResponse extends _BaseResponse { + /// List of channels state returned by the query + List members; + + /// Create a new instance from a json + static QueryMembersResponse fromJson(Map json) => + _$QueryMembersResponseFromJson(json); +} + +/// Model response for [Client.queryUsers] api call +@JsonSerializable(createToJson: false) +class QueryUsersResponse extends _BaseResponse { + /// List of users returned by the query + List users; + + /// Create a new instance from a json + static QueryUsersResponse fromJson(Map json) => + _$QueryUsersResponseFromJson(json); +} + +/// Model response for [channel.getReactions] api call +@JsonSerializable(createToJson: false) +class QueryReactionsResponse extends _BaseResponse { + /// List of reactions returned by the query + List reactions; + + /// Create a new instance from a json + static QueryReactionsResponse fromJson(Map json) => + _$QueryReactionsResponseFromJson(json); +} + +/// Model response for [Channel.getReplies] api call +@JsonSerializable(createToJson: false) +class QueryRepliesResponse extends _BaseResponse { + /// List of messages returned by the api call + List messages; + + /// Create a new instance from a json + static QueryRepliesResponse fromJson(Map json) => + _$QueryRepliesResponseFromJson(json); +} + +/// Model response for [Client.getDevices] api call +@JsonSerializable(createToJson: false) +class ListDevicesResponse extends _BaseResponse { + /// List of user devices + List devices; + + /// Create a new instance from a json + static ListDevicesResponse fromJson(Map json) => + _$ListDevicesResponseFromJson(json); +} + +/// Model response for [Channel.sendFile] api call +@JsonSerializable(createToJson: false) +class SendFileResponse extends _BaseResponse { + /// The url of the uploaded file + String file; + + /// Create a new instance from a json + static SendFileResponse fromJson(Map json) => + _$SendFileResponseFromJson(json); +} + +/// Model response for [Channel.sendImage] api call +@JsonSerializable(createToJson: false) +class SendImageResponse extends _BaseResponse { + /// The url of the uploaded file + String file; + + /// Create a new instance from a json + static SendImageResponse fromJson(Map json) => + _$SendImageResponseFromJson(json); +} + +/// Model response for [Channel.sendReaction] api call +@JsonSerializable(createToJson: false) +class SendReactionResponse extends _BaseResponse { + /// Message returned by the api call + Message message; + + /// The reaction created by the api call + Reaction reaction; + + /// Create a new instance from a json + static SendReactionResponse fromJson(Map json) => + _$SendReactionResponseFromJson(json); +} + +/// Model response for [Client.setGuestUser] api call +@JsonSerializable(createToJson: false) +class SetGuestUserResponse extends _BaseResponse { + /// Guest user access token + String accessToken; + + /// Guest user + User user; + + /// Create a new instance from a json + static SetGuestUserResponse fromJson(Map json) => + _$SetGuestUserResponseFromJson(json); +} + +/// Model response for [Client.updateUser] api call +@JsonSerializable(createToJson: false) +class UpdateUsersResponse extends _BaseResponse { + /// Updated users + Map users; + + /// Create a new instance from a json + static UpdateUsersResponse fromJson(Map json) => + _$UpdateUsersResponseFromJson(json); +} + +/// Model response for [Client.updateMessage] api call +@JsonSerializable(createToJson: false) +class UpdateMessageResponse extends _BaseResponse { + /// Message returned by the api call + Message message; + + /// Create a new instance from a json + static UpdateMessageResponse fromJson(Map json) => + _$UpdateMessageResponseFromJson(json); +} + +/// Model response for [Channel.sendMessage] api call +@JsonSerializable(createToJson: false) +class SendMessageResponse extends _BaseResponse { + /// Message returned by the api call + Message message; + + /// Create a new instance from a json + static SendMessageResponse fromJson(Map json) => + _$SendMessageResponseFromJson(json); +} + +/// Model response for [Client.getMessage] api call +@JsonSerializable(createToJson: false) +class GetMessageResponse extends _BaseResponse { + /// Message returned by the api call + Message message; + + /// Channel of the message + ChannelModel channel; + + /// Create a new instance from a json + static GetMessageResponse fromJson(Map json) { + final res = _$GetMessageResponseFromJson(json); + final jsonChannel = res.message?.extraData?.remove('channel'); + if (jsonChannel != null) { + res.channel = ChannelModel.fromJson(jsonChannel); + } + return res; + } +} + +/// Model response for [Client.search] api call +@JsonSerializable(createToJson: false) +class SearchMessagesResponse extends _BaseResponse { + /// List of messages returned by the api call + List results; + + /// Create a new instance from a json + static SearchMessagesResponse fromJson(Map json) => + _$SearchMessagesResponseFromJson(json); +} + +/// Model response for [Channel.getMessagesById] api call +@JsonSerializable(createToJson: false) +class GetMessagesByIdResponse extends _BaseResponse { + /// Message returned by the api call + List messages; + + /// Create a new instance from a json + static GetMessagesByIdResponse fromJson(Map json) => + _$GetMessagesByIdResponseFromJson(json); +} + +/// Model response for [Channel.update] api call +@JsonSerializable(createToJson: false) +class UpdateChannelResponse extends _BaseResponse { + /// Updated channel + ChannelModel channel; + + /// Channel members + List members; + + /// Message returned by the api call + Message message; + + /// Create a new instance from a json + static UpdateChannelResponse fromJson(Map json) => + _$UpdateChannelResponseFromJson(json); +} + +/// Model response for [Channel.inviteMembers] api call +@JsonSerializable(createToJson: false) +class InviteMembersResponse extends _BaseResponse { + /// Updated channel + ChannelModel channel; + + /// Channel members + List members; + + /// Message returned by the api call + Message message; + + /// Create a new instance from a json + static InviteMembersResponse fromJson(Map json) => + _$InviteMembersResponseFromJson(json); +} + +/// Model response for [Channel.removeMembers] api call +@JsonSerializable(createToJson: false) +class RemoveMembersResponse extends _BaseResponse { + /// Updated channel + ChannelModel channel; + + /// Channel members + List members; + + /// Message returned by the api call + Message message; + + /// Create a new instance from a json + static RemoveMembersResponse fromJson(Map json) => + _$RemoveMembersResponseFromJson(json); +} + +/// Model response for [Channel.sendAction] api call +@JsonSerializable(createToJson: false) +class SendActionResponse extends _BaseResponse { + /// Message returned by the api call + Message message; + + /// Create a new instance from a json + static SendActionResponse fromJson(Map json) => + _$SendActionResponseFromJson(json); +} + +/// Model response for [Channel.addMembers] api call +@JsonSerializable(createToJson: false) +class AddMembersResponse extends _BaseResponse { + /// Updated channel + ChannelModel channel; + + /// Channel members + List members; + + /// Message returned by the api call + Message message; + + /// Create a new instance from a json + static AddMembersResponse fromJson(Map json) => + _$AddMembersResponseFromJson(json); +} + +/// Model response for [Channel.acceptInvite] api call +@JsonSerializable(createToJson: false) +class AcceptInviteResponse extends _BaseResponse { + /// Updated channel + ChannelModel channel; + + /// Channel members + List members; + + /// Message returned by the api call + Message message; + + /// Create a new instance from a json + static AcceptInviteResponse fromJson(Map json) => + _$AcceptInviteResponseFromJson(json); +} + +/// Model response for [Channel.rejectInvite] api call +@JsonSerializable(createToJson: false) +class RejectInviteResponse extends _BaseResponse { + /// Updated channel + ChannelModel channel; + + /// Channel members + List members; + + /// Message returned by the api call + Message message; + + /// Create a new instance from a json + static RejectInviteResponse fromJson(Map json) => + _$RejectInviteResponseFromJson(json); +} + +/// Model response for empty responses +@JsonSerializable(createToJson: false) +class EmptyResponse extends _BaseResponse { + /// Create a new instance from a json + static EmptyResponse fromJson(Map json) => + _$EmptyResponseFromJson(json); +} + +/// Model response for [Channel.query] api call +@JsonSerializable(createToJson: false) +class ChannelStateResponse extends _BaseResponse { + /// Updated channel + ChannelModel channel; + + /// List of messages returned by the api call + List messages; + + /// Channel members + List members; + + /// Number of users watching the channel + int watcherCount; + + /// List of read states + List read; + + /// Create a new instance from a json + static ChannelStateResponse fromJson(Map json) => + _$ChannelStateResponseFromJson(json); +} diff --git a/packages/dart_client/lib/src/api/responses.g.dart b/packages/dart_client/lib/src/api/responses.g.dart new file mode 100644 index 00000000..db5fba9e --- /dev/null +++ b/packages/dart_client/lib/src/api/responses.g.dart @@ -0,0 +1,382 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'responses.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SyncResponse _$SyncResponseFromJson(Map json) { + return SyncResponse() + ..duration = json['duration'] as String + ..events = (json['events'] as List) + ?.map((e) => e == null + ? null + : Event.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(); +} + +QueryChannelsResponse _$QueryChannelsResponseFromJson(Map json) { + return QueryChannelsResponse() + ..duration = json['duration'] as String + ..channels = (json['channels'] as List) + ?.map((e) => e == null ? null : ChannelState.fromJson(e as Map)) + ?.toList(); +} + +TranslateMessageResponse _$TranslateMessageResponseFromJson(Map json) { + return TranslateMessageResponse() + ..duration = json['duration'] as String + ..message = json['message'] == null + ? null + : TranslatedMessage.fromJson((json['message'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )); +} + +QueryMembersResponse _$QueryMembersResponseFromJson(Map json) { + return QueryMembersResponse() + ..duration = json['duration'] as String + ..members = (json['members'] as List) + ?.map((e) => e == null + ? null + : Member.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(); +} + +QueryUsersResponse _$QueryUsersResponseFromJson(Map json) { + return QueryUsersResponse() + ..duration = json['duration'] as String + ..users = (json['users'] as List) + ?.map((e) => e == null + ? null + : User.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(); +} + +QueryReactionsResponse _$QueryReactionsResponseFromJson(Map json) { + return QueryReactionsResponse() + ..duration = json['duration'] as String + ..reactions = (json['reactions'] as List) + ?.map((e) => e == null + ? null + : Reaction.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(); +} + +QueryRepliesResponse _$QueryRepliesResponseFromJson(Map json) { + return QueryRepliesResponse() + ..duration = json['duration'] as String + ..messages = (json['messages'] as List) + ?.map((e) => e == null + ? null + : Message.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(); +} + +ListDevicesResponse _$ListDevicesResponseFromJson(Map json) { + return ListDevicesResponse() + ..duration = json['duration'] as String + ..devices = (json['devices'] as List) + ?.map((e) => e == null + ? null + : Device.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(); +} + +SendFileResponse _$SendFileResponseFromJson(Map json) { + return SendFileResponse() + ..duration = json['duration'] as String + ..file = json['file'] as String; +} + +SendImageResponse _$SendImageResponseFromJson(Map json) { + return SendImageResponse() + ..duration = json['duration'] as String + ..file = json['file'] as String; +} + +SendReactionResponse _$SendReactionResponseFromJson(Map json) { + return SendReactionResponse() + ..duration = json['duration'] as String + ..message = json['message'] == null + ? null + : Message.fromJson((json['message'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )) + ..reaction = json['reaction'] == null + ? null + : Reaction.fromJson((json['reaction'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )); +} + +SetGuestUserResponse _$SetGuestUserResponseFromJson(Map json) { + return SetGuestUserResponse() + ..duration = json['duration'] as String + ..accessToken = json['access_token'] as String + ..user = json['user'] == null + ? null + : User.fromJson((json['user'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )); +} + +UpdateUsersResponse _$UpdateUsersResponseFromJson(Map json) { + return UpdateUsersResponse() + ..duration = json['duration'] as String + ..users = (json['users'] as Map)?.map( + (k, e) => MapEntry( + k as String, + e == null + ? null + : User.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))), + ); +} + +UpdateMessageResponse _$UpdateMessageResponseFromJson(Map json) { + return UpdateMessageResponse() + ..duration = json['duration'] as String + ..message = json['message'] == null + ? null + : Message.fromJson((json['message'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )); +} + +SendMessageResponse _$SendMessageResponseFromJson(Map json) { + return SendMessageResponse() + ..duration = json['duration'] as String + ..message = json['message'] == null + ? null + : Message.fromJson((json['message'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )); +} + +GetMessageResponse _$GetMessageResponseFromJson(Map json) { + return GetMessageResponse() + ..duration = json['duration'] as String + ..message = json['message'] == null + ? null + : Message.fromJson((json['message'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )) + ..channel = json['channel'] == null + ? null + : ChannelModel.fromJson((json['channel'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )); +} + +SearchMessagesResponse _$SearchMessagesResponseFromJson(Map json) { + return SearchMessagesResponse() + ..duration = json['duration'] as String + ..results = (json['results'] as List) + ?.map((e) => e == null ? null : GetMessageResponse.fromJson(e as Map)) + ?.toList(); +} + +GetMessagesByIdResponse _$GetMessagesByIdResponseFromJson(Map json) { + return GetMessagesByIdResponse() + ..duration = json['duration'] as String + ..messages = (json['messages'] as List) + ?.map((e) => e == null + ? null + : Message.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(); +} + +UpdateChannelResponse _$UpdateChannelResponseFromJson(Map json) { + return UpdateChannelResponse() + ..duration = json['duration'] as String + ..channel = json['channel'] == null + ? null + : ChannelModel.fromJson((json['channel'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )) + ..members = (json['members'] as List) + ?.map((e) => e == null + ? null + : Member.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList() + ..message = json['message'] == null + ? null + : Message.fromJson((json['message'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )); +} + +InviteMembersResponse _$InviteMembersResponseFromJson(Map json) { + return InviteMembersResponse() + ..duration = json['duration'] as String + ..channel = json['channel'] == null + ? null + : ChannelModel.fromJson((json['channel'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )) + ..members = (json['members'] as List) + ?.map((e) => e == null + ? null + : Member.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList() + ..message = json['message'] == null + ? null + : Message.fromJson((json['message'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )); +} + +RemoveMembersResponse _$RemoveMembersResponseFromJson(Map json) { + return RemoveMembersResponse() + ..duration = json['duration'] as String + ..channel = json['channel'] == null + ? null + : ChannelModel.fromJson((json['channel'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )) + ..members = (json['members'] as List) + ?.map((e) => e == null + ? null + : Member.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList() + ..message = json['message'] == null + ? null + : Message.fromJson((json['message'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )); +} + +SendActionResponse _$SendActionResponseFromJson(Map json) { + return SendActionResponse() + ..duration = json['duration'] as String + ..message = json['message'] == null + ? null + : Message.fromJson((json['message'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )); +} + +AddMembersResponse _$AddMembersResponseFromJson(Map json) { + return AddMembersResponse() + ..duration = json['duration'] as String + ..channel = json['channel'] == null + ? null + : ChannelModel.fromJson((json['channel'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )) + ..members = (json['members'] as List) + ?.map((e) => e == null + ? null + : Member.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList() + ..message = json['message'] == null + ? null + : Message.fromJson((json['message'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )); +} + +AcceptInviteResponse _$AcceptInviteResponseFromJson(Map json) { + return AcceptInviteResponse() + ..duration = json['duration'] as String + ..channel = json['channel'] == null + ? null + : ChannelModel.fromJson((json['channel'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )) + ..members = (json['members'] as List) + ?.map((e) => e == null + ? null + : Member.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList() + ..message = json['message'] == null + ? null + : Message.fromJson((json['message'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )); +} + +RejectInviteResponse _$RejectInviteResponseFromJson(Map json) { + return RejectInviteResponse() + ..duration = json['duration'] as String + ..channel = json['channel'] == null + ? null + : ChannelModel.fromJson((json['channel'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )) + ..members = (json['members'] as List) + ?.map((e) => e == null + ? null + : Member.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList() + ..message = json['message'] == null + ? null + : Message.fromJson((json['message'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )); +} + +EmptyResponse _$EmptyResponseFromJson(Map json) { + return EmptyResponse()..duration = json['duration'] as String; +} + +ChannelStateResponse _$ChannelStateResponseFromJson(Map json) { + return ChannelStateResponse() + ..duration = json['duration'] as String + ..channel = json['channel'] == null + ? null + : ChannelModel.fromJson((json['channel'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )) + ..messages = (json['messages'] as List) + ?.map((e) => e == null + ? null + : Message.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList() + ..members = (json['members'] as List) + ?.map((e) => e == null + ? null + : Member.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList() + ..watcherCount = json['watcher_count'] as int + ..read = (json['read'] as List) + ?.map((e) => e == null + ? null + : Read.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(); +} diff --git a/packages/dart_client/lib/src/api/retry_policy.dart b/packages/dart_client/lib/src/api/retry_policy.dart new file mode 100644 index 00000000..43af3b8e --- /dev/null +++ b/packages/dart_client/lib/src/api/retry_policy.dart @@ -0,0 +1,37 @@ +import 'package:flutter/foundation.dart'; +import 'package:stream_chat/src/client.dart'; +import 'package:stream_chat/src/exceptions.dart'; + +/// The retry options +class RetryPolicy { + /// Instantiate a new RetryPolicy + RetryPolicy({ + @required this.shouldRetry, + @required this.retryTimeout, + this.attempt, + }); + + /// The number of attempts tried so far + int attempt = 0; + + /// This function evaluates if we should retry the failure + final bool Function(Client client, int attempt, ApiError apiError) + shouldRetry; + + /// In the case that we want to retry a failed request the retryTimeout method is called to determine the timeout + final Duration Function(Client client, int attempt, ApiError apiError) + retryTimeout; + + /// Creates a copy of [RetryPolicy] with specified attributes overridden. + RetryPolicy copyWith({ + bool Function(Client client, int attempt, ApiError apiError) shouldRetry, + Duration Function(Client client, int attempt, ApiError apiError) + retryTimeout, + int attempt, + }) => + RetryPolicy( + retryTimeout: retryTimeout ?? this.retryTimeout, + shouldRetry: shouldRetry ?? this.shouldRetry, + attempt: attempt ?? this.attempt, + ); +} diff --git a/packages/dart_client/lib/src/api/retry_queue.dart b/packages/dart_client/lib/src/api/retry_queue.dart new file mode 100644 index 00000000..12e8377a --- /dev/null +++ b/packages/dart_client/lib/src/api/retry_queue.dart @@ -0,0 +1,202 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; +import 'package:stream_chat/src/api/channel.dart'; +import 'package:stream_chat/src/api/retry_policy.dart'; +import 'package:stream_chat/src/event_type.dart'; +import 'package:stream_chat/src/exceptions.dart'; +import 'package:stream_chat/src/models/message.dart'; +import 'package:stream_chat/stream_chat.dart'; + +/// The retry queue associated to a channel +class RetryQueue { + /// The channel of this queue + final Channel channel; + + /// The logger associated to this queue + final Logger logger; + + /// Instantiate a new RetryQueue object + RetryQueue({ + @required this.channel, + this.logger, + }) { + _retryPolicy = channel.client.retryPolicy; + + _listenConnectionRecovered(); + + _listenFailedEvents(); + } + + final _subscriptions = []; + + void _listenConnectionRecovered() { + _subscriptions + .add(channel.client.on(EventType.connectionRecovered).listen((event) { + if (!_isRetrying && event.online) { + _startRetrying(); + } + })); + } + + final HeapPriorityQueue _messageQueue = HeapPriorityQueue(_byDate); + bool _isRetrying = false; + RetryPolicy _retryPolicy; + + /// Add a list of messages + void add(List messages) { + final messageList = _messageQueue.toList(); + _messageQueue.addAll(messages + .where((element) => !messageList.any((m) => m.id == element.id))); + + if (_messageQueue.isNotEmpty && !_isRetrying) { + _startRetrying(); + } + } + + Future _startRetrying() async { + logger?.info('start retrying'); + _isRetrying = true; + final retryPolicy = _retryPolicy.copyWith(attempt: 0); + + while (_messageQueue.isNotEmpty) { + final message = _messageQueue.first; + try { + logger?.info('retry attempt ${retryPolicy.attempt}'); + await _sendMessage(message); + logger?.info('message sent - removing it from the queue'); + _messageQueue.remove(message); + logger?.info('now ${_messageQueue.length} messages in the queue'); + retryPolicy.attempt = 0; + } catch (error) { + ApiError apiError; + if (error is DioError) { + if (error.type == DioErrorType.RESPONSE) { + _messageQueue.remove(message); + return; + } + apiError = ApiError( + error.response?.data, + error.response?.statusCode, + ); + } else if (error is ApiError) { + apiError = error; + if (apiError.status?.toString()?.startsWith('4') == true) { + _messageQueue.remove(message); + return; + } + } + + if (!retryPolicy.shouldRetry( + channel.client, + retryPolicy.attempt, + apiError, + )) { + _messageQueue.toList().forEach(_sendFailedEvent); + _isRetrying = false; + return; + } + + retryPolicy.attempt++; + final timeout = retryPolicy.retryTimeout( + channel.client, + retryPolicy.attempt, + apiError, + ); + await Future.delayed(timeout); + } + } + _isRetrying = false; + } + + void _sendFailedEvent(Message message) { + final newStatus = message.status == MessageSendingStatus.SENDING + ? MessageSendingStatus.FAILED + : (message.status == MessageSendingStatus.UPDATING + ? MessageSendingStatus.FAILED_UPDATE + : MessageSendingStatus.FAILED_DELETE); + channel.state.addMessage(message.copyWith( + status: newStatus, + )); + } + + Future _sendMessage(Message message) async { + if (message.status == MessageSendingStatus.FAILED_UPDATE || + message.status == MessageSendingStatus.UPDATING) { + await channel.client.updateMessage( + message, + channel.cid, + ); + } else if (message.status == MessageSendingStatus.FAILED || + message.status == MessageSendingStatus.SENDING) { + await channel.sendMessage( + message, + ); + } else if (message.status == MessageSendingStatus.FAILED_DELETE || + message.status == MessageSendingStatus.DELETING) { + await channel.client.deleteMessage( + message, + channel.cid, + ); + } + } + + void _listenFailedEvents() { + _subscriptions.add(channel.on().listen((event) { + final messageList = _messageQueue.toList(); + if (event.message != null) { + final messageIndex = + messageList.indexWhere((m) => m.id == event.message.id); + if (messageIndex == -1 && + [ + MessageSendingStatus.FAILED_UPDATE, + MessageSendingStatus.FAILED, + MessageSendingStatus.FAILED_DELETE, + ].contains(event.message.status)) { + logger?.info('add message from events'); + add([event.message]); + } else if (messageIndex != -1 && + [ + MessageSendingStatus.SENT, + null, + ].contains(event.message.status)) { + _messageQueue.remove(messageList[messageIndex]); + } + } + })); + } + + /// Call this method to dispose this object + void dispose() { + _messageQueue.clear(); + _subscriptions.forEach((s) => s.cancel()); + } + + static int _byDate(Message m1, Message m2) { + final date1 = _getMessageDate(m1); + final date2 = _getMessageDate(m2); + + return date1.compareTo(date2); + } + + static DateTime _getMessageDate(Message m1) { + switch (m1.status) { + case MessageSendingStatus.FAILED_DELETE: + case MessageSendingStatus.DELETING: + return m1.deletedAt; + + case MessageSendingStatus.FAILED: + case MessageSendingStatus.SENDING: + return m1.createdAt; + + case MessageSendingStatus.FAILED_UPDATE: + case MessageSendingStatus.UPDATING: + return m1.updatedAt; + default: + return null; + } + } +} diff --git a/packages/dart_client/lib/src/api/web_socket_channel_html.dart b/packages/dart_client/lib/src/api/web_socket_channel_html.dart new file mode 100644 index 00000000..9ddd83e5 --- /dev/null +++ b/packages/dart_client/lib/src/api/web_socket_channel_html.dart @@ -0,0 +1,7 @@ +import 'package:web_socket_channel/html.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +/// Html version of websocket implementation +/// Used in Flutter web version +WebSocketChannel connectWebSocket(String url, {Iterable protocols}) => + HtmlWebSocketChannel.connect(url, protocols: protocols); diff --git a/packages/dart_client/lib/src/api/web_socket_channel_io.dart b/packages/dart_client/lib/src/api/web_socket_channel_io.dart new file mode 100644 index 00000000..ed37ba7f --- /dev/null +++ b/packages/dart_client/lib/src/api/web_socket_channel_io.dart @@ -0,0 +1,7 @@ +import 'package:web_socket_channel/io.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +/// IO version of websocket implementation +/// Used in Flutter mobile version +WebSocketChannel connectWebSocket(String url, {Iterable protocols}) => + IOWebSocketChannel.connect(url, protocols: protocols); diff --git a/packages/dart_client/lib/src/api/web_socket_channel_stub.dart b/packages/dart_client/lib/src/api/web_socket_channel_stub.dart new file mode 100644 index 00000000..7e2e47bd --- /dev/null +++ b/packages/dart_client/lib/src/api/web_socket_channel_stub.dart @@ -0,0 +1,9 @@ +import 'package:web_socket_channel/web_socket_channel.dart'; + +/// Stub version of websocket implementation +/// Used just for conditional library import +WebSocketChannel connectWebSocket(String url, + {Iterable protocols, + Map headers, + Duration pingInterval}) => + throw UnimplementedError(); diff --git a/packages/dart_client/lib/src/api/websocket.dart b/packages/dart_client/lib/src/api/websocket.dart new file mode 100644 index 00000000..7d904a12 --- /dev/null +++ b/packages/dart_client/lib/src/api/websocket.dart @@ -0,0 +1,295 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +import '../models/event.dart'; +import '../models/user.dart'; +import 'connection_status.dart'; +import 'web_socket_channel_stub.dart' + if (dart.library.html) 'web_socket_channel_html.dart' + if (dart.library.io) 'web_socket_channel_io.dart'; + +typedef EventHandler = void Function(Event); +typedef ConnectWebSocket = WebSocketChannel Function(String url, + {Iterable protocols}); + +// TODO: parse error even +// TODO: if parsing an error into an event fails we should not hide the original error +/// A WebSocket connection that reconnects upon failure. +class WebSocket { + /// Creates a new websocket + /// To connect the WS call [connect] + WebSocket({ + @required this.baseUrl, + this.user, + this.connectParams, + this.connectPayload, + this.handler, + this.logger, + this.connectFunc = connectWebSocket, + this.reconnectionMonitorInterval = 1, + this.healthCheckInterval = 20, + this.reconnectionMonitorTimeout = 40, + }) { + final qs = Map.from(connectParams); + + final data = Map.from(connectPayload); + + data['user_details'] = user.toJson(); + qs['json'] = json.encode(data); + + if (baseUrl.startsWith('https')) { + _path = baseUrl.replaceFirst('https://', ''); + _path = Uri.https(_path, 'connect', qs) + .toString() + .replaceFirst('https', 'wss'); + } else if (baseUrl.startsWith('http')) { + _path = baseUrl.replaceFirst('http://', ''); + _path = + Uri.http(_path, 'connect', qs).toString().replaceFirst('http', 'ws'); + } else { + _path = Uri.https(baseUrl, 'connect', qs) + .toString() + .replaceFirst('https', 'wss'); + } + } + + /// WS base url + final String baseUrl; + + /// User performing the WS connection + final User user; + + /// Querystring connection parameters + final Map connectParams; + + /// WS connection payload + final Map connectPayload; + + /// Functions that will be called every time a new event is received from the connection + final EventHandler handler; + + /// A WS specific logger instance + final Logger logger; + + /// Connection function + /// Used only for testing purpose + @visibleForTesting + final ConnectWebSocket connectFunc; + + /// Interval of the reconnection monitor timer + /// This checks that it received a new event in the last [reconnectionMonitorTimeout] seconds, + /// otherwise it considers the connection unhealthy and reconnects the WS + final int reconnectionMonitorInterval; + + /// Interval of the health event sending timer + /// This sends a health event every [healthCheckInterval] seconds in order to + /// make the server aware that the client is still listening + final int healthCheckInterval; + + /// The timeout that uses the reconnection monitor timer to consider the connection unhealthy + final int reconnectionMonitorTimeout; + + /// This notifies of connection status changes + final ValueNotifier connectionStatus = + ValueNotifier(ConnectionStatus.disconnected); + + String _path; + int _retryAttempt = 1; + WebSocketChannel _channel; + Timer _healthCheck, _reconnectionMonitor; + DateTime _lastEventAt; + bool _manuallyClosed = false, _connecting = false, _reconnecting = false; + + Event _decodeEvent(String source) { + return Event.fromJson(json.decode(source)); + } + + Completer _connectionCompleter = Completer(); + + /// Connect the WS using the parameters passed in the constructor + Future connect() { + _manuallyClosed = false; + + if (_connecting) { + logger.severe('already connecting'); + return null; + } + + _connecting = true; + connectionStatus.value = ConnectionStatus.connecting; + + logger.info('connecting to $_path'); + + _channel = connectFunc(_path); + _channel.stream.listen( + (data) { + final jsonData = json.decode(data); + if (jsonData['error'] != null) { + return _onConnectionError(jsonData['error']); + } + _onData(data); + }, + onError: (error, stacktrace) { + _onConnectionError(error, stacktrace); + }, + onDone: () { + _onDone(); + }, + ); + return _connectionCompleter.future; + } + + void _onDone() { + _connecting = false; + if (_manuallyClosed) { + return; + } + + logger.info( + 'connection closed | closeCode: ${_channel.closeCode} | closedReason: ${_channel.closeReason}'); + + if (!_reconnecting) { + _reconnect(); + } + } + + void _onData(data) { + final event = _decodeEvent(data); + logger.info('received new event: $data'); + + if (_lastEventAt == null) { + logger.info('connection estabilished'); + _connecting = false; + _reconnecting = false; + _lastEventAt = DateTime.now(); + + connectionStatus.value = ConnectionStatus.connected; + _retryAttempt = 1; + + if (!_connectionCompleter.isCompleted) { + _connectionCompleter.complete(event); + } + + _startReconnectionMonitor(); + _startHealthCheck(); + } + + handler(event); + _lastEventAt = DateTime.now(); + } + + Future _onConnectionError(error, [stacktrace]) async { + logger.severe('error connecting'); + logger.severe(error); + if (stacktrace != null) { + logger.severe(stacktrace); + } + _connecting = false; + + if (!_reconnecting) { + connectionStatus.value = ConnectionStatus.disconnected; + } + + if (!_connectionCompleter.isCompleted) { + _cancelTimers(); + _connectionCompleter.completeError(error, stacktrace); + } else if (!_reconnecting) { + return _reconnect(); + } + } + + void _startReconnectionMonitor() { + final reconnectionTimer = (_) { + final now = DateTime.now(); + if (_lastEventAt != null && + now.difference(_lastEventAt).inSeconds > reconnectionMonitorTimeout) { + _channel.sink.close(); + } + }; + + _reconnectionMonitor = Timer.periodic( + Duration(seconds: reconnectionMonitorInterval), + reconnectionTimer, + ); + + reconnectionTimer(_reconnectionMonitor); + } + + void _reconnectTimer() async { + if (!_reconnecting) { + return; + } + if (_connecting) { + logger.info('already connecting'); + return; + } + + logger.info('reconnecting..'); + + _cancelTimers(); + + try { + await connect(); + } catch (e) {} + await Future.delayed( + Duration(seconds: min(_retryAttempt * 5, 25)), + () { + _reconnectTimer(); + _retryAttempt++; + }, + ); + } + + Future _reconnect() async { + logger.info('reconnect'); + if (!_reconnecting) { + _reconnecting = true; + connectionStatus.value = ConnectionStatus.connecting; + } + + _reconnectTimer(); + } + + void _cancelTimers() { + _lastEventAt = null; + if (_healthCheck != null) { + _healthCheck.cancel(); + } + if (_reconnectionMonitor != null) { + _reconnectionMonitor.cancel(); + } + } + + void _startHealthCheck() { + logger.info('start health check monitor'); + + final healthCheckTimer = (_) { + logger.info('sending health.check'); + _channel.sink.add("{'type': 'health.check'}"); + }; + + _healthCheck = Timer.periodic( + Duration(seconds: healthCheckInterval), + healthCheckTimer, + ); + + healthCheckTimer(_healthCheck); + } + + /// Disconnects the WS and releases eventual resources + Future disconnect() { + logger.info('disconnecting'); + _connectionCompleter = Completer(); + _cancelTimers(); + _manuallyClosed = true; + connectionStatus.value = ConnectionStatus.disconnected; + connectionStatus.dispose(); + return _channel.sink.close(); + } +} diff --git a/packages/dart_client/lib/src/client.dart b/packages/dart_client/lib/src/client.dart new file mode 100644 index 00000000..0952447a --- /dev/null +++ b/packages/dart_client/lib/src/client.dart @@ -0,0 +1,1388 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:logging/logging.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:stream_chat/src/api/retry_policy.dart'; +import 'package:stream_chat/src/event_type.dart'; +import 'package:stream_chat/src/models/channel_model.dart'; +import 'package:stream_chat/src/models/own_user.dart'; +import 'package:stream_chat/version.dart'; +import 'package:uuid/uuid.dart'; + +import 'api/channel.dart'; +import 'api/connection_status.dart'; +import 'api/requests.dart'; +import 'api/responses.dart'; +import 'api/websocket.dart'; +import 'db/offline_storage.dart'; +import 'exceptions.dart'; +import 'models/event.dart'; +import 'models/message.dart'; +import 'models/user.dart'; + +typedef LogHandlerFunction = void Function(LogRecord record); +typedef DecoderFunction = T Function(Map); +typedef TokenProvider = Future Function(String userId); + +/// The key used to save the userId to sharedPreferences +const String KEY_USER_ID = 'KEY_USER_ID'; + +/// The key used to save the token to sharedPreferences +const String KEY_TOKEN = 'KEY_TOKEN'; + +/// The key used to save the apiKey to sharedPreferences +const String KEY_API_KEY = 'KEY_API_KEY'; + +/// The official Dart client for Stream Chat, +/// a service for building chat applications. +/// This library can be used on any Dart project and on both mobile and web apps with Flutter. +/// You can sign up for a Stream account at https://getstream.io/chat/ +/// +/// The Chat client will manage API call, event handling and manage the +/// websocket connection to Stream Chat servers. +/// +/// ```dart +/// final client = Client("stream-chat-api-key"); +/// ``` +class Client { + /// Create a client instance with default options. + /// You should only create the client once and re-use it across your application. + Client( + this.apiKey, { + this.tokenProvider, + this.baseURL = _defaultBaseURL, + this.logLevel = Level.WARNING, + this.logHandlerFunction, + this.persistenceEnabled = true, + Duration connectTimeout = const Duration(seconds: 6), + Duration receiveTimeout = const Duration(seconds: 6), + Dio httpClient, + this.showLocalNotification, + this.backgroundKeepAlive = const Duration(minutes: 1), + RetryPolicy retryPolicy, + }) { + WidgetsFlutterBinding.ensureInitialized(); + + _retryPolicy ??= RetryPolicy( + retryTimeout: (Client client, int attempt, ApiError error) => + Duration(seconds: 1 * attempt), + shouldRetry: (Client client, int attempt, ApiError error) => attempt < 5, + ); + + state = ClientState(this); + + _setupLogger(); + _setupDio(httpClient, receiveTimeout, connectTimeout); + + logger.info('instantiating new client'); + } + + OfflineStorage _offlineStorage; + + /// If true chat data will persist on disk + final bool persistenceEnabled; + + RetryPolicy _retryPolicy; + + bool _synced = false; + + /// The retry policy options getter + RetryPolicy get retryPolicy => _retryPolicy; + + /// Method used to show a local notification while the app is in background + /// Switching to another application will not disconnect the client immediately + /// So, use this method to show the notification when receiving a new message via events + final void Function(Message, ChannelModel) showLocalNotification; + + /// The amount of time that will pass before disconnecting the client in the background + final Duration backgroundKeepAlive; + + /// Client offline database + OfflineStorage get offlineStorage => _offlineStorage; + + /// This client state + ClientState state; + + /// By default the Chat Client will write all messages with level Warn or Error to stdout. + /// During development you might want to enable more logging information, you can change the default log level when constructing the client. + /// + /// ```dart + /// final client = Client("stream-chat-api-key", logLevel: Level.INFO); + /// ``` + final Level logLevel; + + /// Client specific logger instance. + /// Refer to the class [Logger] to learn more about the specific implementation. + final Logger logger = Logger.detached('📡'); + + /// A function that has a parameter of type [LogRecord]. + /// This is called on every new log record. + /// By default the client will use the handler returned by [_getDefaultLogHandler]. + /// Setting it you can handle the log messages directly instead of have them written to stdout, + /// this is very convenient if you use an error tracking tool or if you want to centralize your logs into one facility. + /// + /// ```dart + /// myLogHandlerFunction = (LogRecord record) { + /// // do something with the record (ie. send it to Sentry or Fabric) + /// } + /// + /// final client = Client("stream-chat-api-key", logHandlerFunction: myLogHandlerFunction); + ///``` + LogHandlerFunction logHandlerFunction; + + /// Your project Stream Chat api key. + /// Find your API keys here https://getstream.io/dashboard/ + String apiKey; + + /// Your project Stream Chat base url. + final String baseURL; + + /// A function in which you send a request to your own backend to get a Stream Chat API token. + /// The token will be the return value of the function. + /// It's used by the client to refresh the token once expired or to set the user without a predefined token using [setUserWithProvider]. + final TokenProvider tokenProvider; + + /// [Dio] httpClient + /// It's be chosen because it's easy to use and supports interesting features out of the box + /// (Interceptors, Global configuration, FormData, File downloading etc.) + @visibleForTesting + Dio httpClient = Dio(); + + static const _defaultBaseURL = 'chat-us-east-1.stream-io-api.com'; + static const _tokenExpiredErrorCode = 40; + VoidCallback _connectionStatusListener; + + final BehaviorSubject _controller = BehaviorSubject(); + + /// Stream of [Event] coming from websocket connection + /// Listen to this or use the [on] method to filter specific event types + Stream get stream => _controller.stream; + + /// This notifies the connection status of the websocket connection. + /// Listen to this to get notified when the websocket tries to reconnect. + final ValueNotifier wsConnectionStatus = + ValueNotifier(ConnectionStatus.disconnected); + + /// The current user token + String token; + + /// The id of the current websocket connection + String get connectionId => _connectionId; + + bool _anonymous = false; + String _connectionId; + WebSocket _ws; + + bool get _hasConnectionId => _connectionId != null; + + void _setupDio( + Dio httpClient, + Duration receiveTimeout, + Duration connectTimeout, + ) { + logger.info('http client setup'); + + this.httpClient = httpClient ?? Dio(); + + String url; + if (!baseURL.startsWith('https') && !baseURL.startsWith('http')) { + url = Uri.https(baseURL, '').toString(); + } else { + url = baseURL; + } + + this.httpClient.options.baseUrl = url; + this.httpClient.options.receiveTimeout = receiveTimeout.inMilliseconds; + this.httpClient.options.connectTimeout = connectTimeout.inMilliseconds; + this.httpClient.interceptors.add( + InterceptorsWrapper( + onRequest: (options) async { + options.queryParameters.addAll(_commonQueryParams); + options.headers.addAll(_httpHeaders); + + if (_connectionId != null && + (options.data is Map || options.data == null)) { + options.data = { + 'connection_id': _connectionId, + ...(options.data ?? {}), + }; + } + + var stringData = options.data.toString(); + + if (options.data is FormData) { + final multiPart = (options.data as FormData).files[0]?.value; + stringData = + '${multiPart?.filename} - ${multiPart?.contentType}'; + } + + logger.info(''' + + method: ${options.method} + url: ${options.uri} + headers: ${options.headers} + data: $stringData + + '''); + + return options; + }, + onError: _tokenExpiredInterceptor, + ), + ); + } + + Future _tokenExpiredInterceptor(DioError err) async { + final apiError = ApiError( + err.response?.data, + err.response?.statusCode, + ); + + if (apiError.code == _tokenExpiredErrorCode) { + logger.info('token expired'); + + if (tokenProvider != null) { + httpClient.lock(); + final userId = state.user.id; + + _ws.connectionStatus.removeListener(_connectionStatusListener); + + await _disconnect(); + + final newToken = await tokenProvider(userId); + await Future.delayed(Duration(seconds: 4)); + token = newToken; + + httpClient.unlock(); + + await setUser(User(id: userId), newToken); + + try { + return await httpClient.request( + err.request.path, + cancelToken: err.request.cancelToken, + data: err.request.data, + onReceiveProgress: err.request.onReceiveProgress, + onSendProgress: err.request.onSendProgress, + queryParameters: err.request.queryParameters, + options: err.request, + ); + } catch (err) { + return err; + } + } + } + + return err; + } + + LogHandlerFunction _getDefaultLogHandler() { + final levelEmojiMapper = { + Level.INFO.name: 'ℹ️', + Level.WARNING.name: '⚠️', + Level.SEVERE.name: '🚨', + }; + return (LogRecord record) { + print( + '(${record.time}) ${levelEmojiMapper[record.level.name] ?? record.level.name} ${record.loggerName} ${record.message}'); + if (record.stackTrace != null) { + print(record.stackTrace); + } + }; + } + + Logger _detachedLogger( + String name, + ) { + return Logger.detached(name) + ..level = logLevel + ..onRecord.listen(logHandlerFunction ?? _getDefaultLogHandler()); + } + + void _setupLogger() { + logger.level = logLevel; + + logHandlerFunction ??= _getDefaultLogHandler(); + + logger.onRecord.listen(logHandlerFunction); + + logger.info('logger setup'); + } + + /// Call this function to dispose the client + void dispose() async { + await _offlineStorage?.disconnect(); + await _disconnect(); + httpClient.close(); + await _controller.close(); + state.channels.values.forEach((c) => c.dispose()); + state.dispose(); + } + + Map get _httpHeaders => { + 'Authorization': token, + 'stream-auth-type': _authType, + 'x-stream-client': _userAgent, + 'Content-Encoding': 'gzip', + }; + + /// Set the current user, this triggers a connection to the API. + /// It returns a [Future] that resolves when the connection is setup. + Future setUser(User user, String token) async { + if (_connectCompleter != null && !_connectCompleter.isCompleted) { + logger.warning('Already connecting'); + throw Exception('Already connecting'); + } + + _connectCompleter = Completer(); + + logger.info('set user'); + state.user = OwnUser.fromJson(user.toJson()); + this.token = token; + _anonymous = false; + + final sharedPreferences = await SharedPreferences.getInstance(); + await sharedPreferences.setString(KEY_USER_ID, user.id); + await sharedPreferences.setString(KEY_TOKEN, token); + await sharedPreferences.setString(KEY_API_KEY, apiKey); + + return connect().then((event) { + _connectCompleter.complete(event); + return event; + }).catchError((e, s) { + _connectCompleter.completeError(e, s); + throw e; + }); + } + + /// Set the current user using the [tokenProvider] to fetch the token. + /// It returns a [Future] that resolves when the connection is setup. + Future setUserWithProvider(User user) async { + if (tokenProvider == null) { + throw Exception(''' + TokenProvider must be provided in the constructor in order to use `setUserWithProvider` method. + Use `setUser` providing a token. + '''); + } + final token = await tokenProvider(user.id); + return setUser(user, token); + } + + /// Stream of [Event] coming from websocket connection + /// Pass an eventType as parameter in order to filter just a type of event + Stream on([ + String eventType, + String eventType2, + String eventType3, + String eventType4, + ]) => + stream.where((event) => + eventType == null || + (event.type != null && + (event.type == eventType || + event.type == eventType2 || + event.type == eventType3 || + event.type == eventType4))); + + /// Method called to add a new event to the [_controller]. + void handleEvent(Event event) async { + logger.info('handle new event: ${event.toJson()}'); + if (event.connectionId != null) { + _connectionId = event.connectionId; + } + + if (!event.isLocal) { + if (_synced && event.createdAt != null) { + await _offlineStorage?.updateConnectionInfo(event); + await _offlineStorage?.updateLastSyncAt(event.createdAt); + } + } + + if (event.user != null) { + state._updateUser(event.user); + } + + if (event.me != null) { + state.user = event.me; + } + _controller.add(event); + } + + Completer _connectCompleter; + + /// Connect the client websocket + Future connect() async { + logger.info('connecting'); + if (wsConnectionStatus.value == ConnectionStatus.connecting) { + logger.warning('Already connecting'); + throw Exception('Already connecting'); + } + + if (wsConnectionStatus.value == ConnectionStatus.connected) { + logger.warning('Already connected'); + throw Exception('Already connected'); + } + + wsConnectionStatus.value = ConnectionStatus.connecting; + + if (persistenceEnabled && _offlineStorage == null) { + _offlineStorage = + await connectDatabase(state.user, _detachedLogger('💽')); + } + + _ws = WebSocket( + baseUrl: baseURL, + user: state.user, + connectParams: { + 'api_key': apiKey, + 'authorization': token, + 'stream-auth-type': _authType, + 'x-stream-client': _userAgent, + }, + connectPayload: { + 'user_id': state.user.id, + 'server_determines_connection_id': true, + }, + handler: handleEvent, + logger: _detachedLogger('🔌'), + ); + + _connectionStatusListener = () async { + final value = _ws.connectionStatus.value; + wsConnectionStatus.value = value; + handleEvent(Event( + type: EventType.connectionChanged, + online: value == ConnectionStatus.connected, + )); + + if (value == ConnectionStatus.connected && + state.channels?.isNotEmpty == true) { + queryChannels(filter: { + 'cid': { + '\$in': state.channels.keys.toList(), + }, + }).then( + (_) async { + await resync(); + handleEvent(Event( + type: EventType.connectionRecovered, + online: true, + )); + }, + ); + } else { + _synced = false; + } + }; + + _ws.connectionStatus.addListener(_connectionStatusListener); + + var event = await _offlineStorage?.getConnectionInfo(); + + await _ws.connect().then((e) async { + await _offlineStorage?.updateConnectionInfo(e); + event = e; + await resync(); + }).catchError((err, stacktrace) { + logger.severe('error connecting ws', err, stacktrace); + if (err is Map) { + throw err; + } + }); + + return event; + } + + /// Get the events missed while offline to sync the offline storage + Future resync([List cids]) async { + final lastSyncAt = await offlineStorage?.getLastSyncAt(); + + if (lastSyncAt == null) { + _synced = true; + return; + } + + cids ??= await offlineStorage?.getChannelCids(); + + if (cids?.isEmpty == true) { + return; + } + + try { + final rawRes = await post('/sync', data: { + 'channel_cids': cids, + 'last_sync_at': lastSyncAt.toUtc().toIso8601String(), + }); + logger.fine('rawRes: $rawRes'); + + final res = decode( + rawRes.data, + SyncResponse.fromJson, + ); + + res.events.sort((a, b) => a.createdAt.compareTo(b.createdAt)); + + res.events.forEach((element) { + logger.fine('element.type: ${element.type}'); + logger.fine('element.message.text: ${element.message?.text}'); + }); + + res.events.forEach((event) { + handleEvent(event); + }); + + await _offlineStorage?.updateLastSyncAt(DateTime.now()); + _synced = true; + } catch (error) { + logger.severe('Error during resync $error'); + } + } + + String _asMap(sort) { + return sort?.map((s) => s.toJson().toString())?.join(''); + } + + final _queryChannelsStreams = >>{}; + + /// Requests channels with a given query. + Future> queryChannels({ + Map filter, + List sort, + Map options, + PaginationParams paginationParams = const PaginationParams(limit: 10), + int messageLimit, + bool onlyOffline = false, + bool waitForConnect = true, + }) async { + if (waitForConnect) { + if (_connectCompleter != null && !_connectCompleter.isCompleted) { + logger.info('awaiting connection completer'); + await _connectCompleter.future; + } + if (wsConnectionStatus.value != ConnectionStatus.connected) { + throw Exception( + 'You cannot use queryChannels without an active connection. Call setUser to connect the client.'); + } + } + + final hash = base64.encode(utf8.encode( + '$filter${_asMap(sort)}$options${paginationParams?.toJson()}$messageLimit$onlyOffline')); + if (_queryChannelsStreams.containsKey(hash)) { + return _queryChannelsStreams[hash]; + } + + final newQueryChannelsStream = _doQueryChannels( + filter: filter, + sort: sort, + options: options, + paginationParams: paginationParams, + messageLimit: messageLimit, + onlyOffline: onlyOffline, + ).whenComplete(() { + _queryChannelsStreams.remove(hash); + }); + + _queryChannelsStreams[hash] = newQueryChannelsStream; + + return newQueryChannelsStream; + } + + Future> _doQueryChannels({ + @required Map filter, + @required List sort, + @required Map options, + @required int messageLimit, + PaginationParams paginationParams = const PaginationParams(limit: 10), + bool onlyOffline = false, + }) async { + logger.info('Query channel start'); + final defaultOptions = { + 'state': true, + 'watch': true, + 'presence': false, + }; + + var payload = { + 'filter_conditions': filter, + 'sort': sort, + }; + + if (messageLimit != null) { + payload['message_limit'] = messageLimit; + } + + payload.addAll(defaultOptions); + + if (options != null) { + payload.addAll(options); + } + + if (paginationParams != null) { + payload.addAll(paginationParams.toJson()); + } + + if (onlyOffline) { + return _queryChannelsOffline( + filter: filter, + sort: sort, + paginationParams: paginationParams, + ); + } + + try { + final response = await get( + '/channels', + queryParameters: { + 'payload': jsonEncode(payload), + }, + ); + + final res = decode( + response.data, + QueryChannelsResponse.fromJson, + ); + + final users = res.channels + ?.expand((channel) => channel.members.map((member) => member.user)) + ?.toList(); + + if (users != null) { + state._updateUsers(users); + } + + logger.info('Got ${res.channels?.length} channels from api'); + + if (res.channels?.isEmpty != false && + (paginationParams?.offset ?? 0) == 0) { + logger.warning('''We could not find any channel for this query. + Please make sure to take a look at the Flutter tutorial: https://getstream.io/chat/flutter/tutorial + If your application already has users and channels, you might need to adjust your query channel as explained in the docs https://getstream.io/chat/docs/query_channels/?language=dart'''); + } + + final newChannels = Map.from(state.channels ?? {}); + final channels = []; + + if (res.channels != null) { + for (final channelState in res.channels) { + final channel = newChannels[channelState.channel.cid]; + if (channel != null) { + channel.state?.updateChannelState(channelState); + channels.add(channel); + } else { + final newChannel = Channel.fromState(this, channelState); + await _offlineStorage + ?.updateChannelState(newChannel.state.channelState); + newChannel.state?.updateChannelState(channelState); + newChannels[newChannel.cid] = newChannel; + channels.add(newChannel); + } + } + } + + state.channels = newChannels; + + await _offlineStorage?.updateChannelQueries( + filter, + res.channels.map((c) => c.channel.cid).toList(), + paginationParams?.offset == null || paginationParams.offset == 0, + ); + + return channels; + } catch (e) { + if (!persistenceEnabled) { + rethrow; + } + return _queryChannelsOffline( + filter: filter, + sort: sort, + paginationParams: paginationParams, + ); + } + } + + _parseError(DioError error) { + if (error.type == DioErrorType.RESPONSE) { + final apiError = + ApiError(error.response?.data, error.response?.statusCode); + logger.severe('apiError: ${apiError.toString()}'); + return apiError; + } + + return error; + } + + Future> _queryChannelsOffline({ + @required Map filter, + @required List sort, + PaginationParams paginationParams = const PaginationParams(limit: 10), + }) async { + final offlineChannels = await _offlineStorage?.getChannelStates( + filter: filter, + sort: sort, + paginationParams: paginationParams, + ) ?? + []; + final newChannels = Map.from(state.channels ?? {}); + logger.info('Got ${offlineChannels.length} channels from storage'); + final channels = offlineChannels.map((channelState) { + final channel = newChannels[channelState.channel.cid]; + if (channel != null) { + channel.state?.updateChannelState(channelState); + return channel; + } else { + final newChannel = Channel.fromState(this, channelState); + _offlineStorage?.updateChannelState(newChannel.state.channelState); + newChannels[newChannel.cid] = newChannel; + return newChannel; + } + }).toList(); + + if (channels.isNotEmpty) { + state.channels = newChannels; + } + return channels; + } + + /// Handy method to make http GET request with error parsing. + Future> get( + String path, { + Map queryParameters, + }) async { + try { + final response = await httpClient.get( + path, + queryParameters: queryParameters, + ); + return response; + } on DioError catch (error) { + throw _parseError(error); + } + } + + /// Handy method to make http POST request with error parsing. + Future> post( + String path, { + dynamic data, + }) async { + try { + final response = await httpClient.post(path, data: data); + return response; + } on DioError catch (error) { + throw _parseError(error); + } + } + + /// Handy method to make http DELETE request with error parsing. + Future> delete( + String path, { + Map queryParameters, + }) async { + try { + final response = await httpClient.delete(path, + queryParameters: queryParameters); + return response; + } on DioError catch (error) { + throw _parseError(error); + } + } + + /// Handy method to make http PATCH request with error parsing. + Future> patch( + String path, { + Map queryParameters, + dynamic data, + }) async { + try { + final response = await httpClient.patch( + path, + queryParameters: queryParameters, + data: data, + ); + return response; + } on DioError catch (error) { + throw _parseError(error); + } + } + + /// Handy method to make http PUT request with error parsing. + Future> put( + String path, { + Map queryParameters, + dynamic data, + }) async { + try { + final response = await httpClient.put( + path, + queryParameters: queryParameters, + data: data, + ); + return response; + } on DioError catch (error) { + throw _parseError(error); + } + } + + /// Used to log errors and stacktrace in case of bad json deserialization + T decode(String j, DecoderFunction decoderFunction) { + try { + if (j == null) { + return null; + } + return decoderFunction(json.decode(j)); + } catch (error, stacktrace) { + logger.severe('Error decoding response', error, stacktrace); + rethrow; + } + } + + String get _authType => _anonymous ? 'anonymous' : 'jwt'; + + // TODO: get the right version of the lib from the build toolchain + String get _userAgent => 'stream-chat-dart-client-$PACKAGE_VERSION'; + + Map get _commonQueryParams => { + 'user_id': state.user?.id, + 'api_key': apiKey, + 'connection_id': _connectionId, + }; + + /// Set the current user with an anonymous id, this triggers a connection to the API. + /// It returns a [Future] that resolves when the connection is setup. + Future setAnonymousUser() async { + if (_connectCompleter != null && !_connectCompleter.isCompleted) { + logger.warning('Already connecting'); + throw Exception('Already connecting'); + } + + _connectCompleter = Completer(); + + _anonymous = true; + final uuid = Uuid(); + state.user = OwnUser(id: uuid.v4()); + + return connect().then((event) { + _connectCompleter.complete(event); + return event; + }).catchError((e, s) { + _connectCompleter.completeError(e, s); + throw e; + }); + } + + /// Set the current user as guest, this triggers a connection to the API. + /// It returns a [Future] that resolves when the connection is setup. + Future setGuestUser(User user) async { + _anonymous = true; + final response = await post('/guest', data: {'user': user.toJson()}) + .then((res) => decode( + res.data, SetGuestUserResponse.fromJson)) + .whenComplete(() => _anonymous = false); + return setUser( + response.user, + response.accessToken, + ); + } + + /// Closes the websocket connection and resets the client + /// If [flushOfflineStorage] is true the client deletes all offline user's data + /// If [clearUser] is true the client unsets the current user + Future disconnect({ + bool flushOfflineStorage = false, + bool clearUser = false, + }) async { + logger.info( + 'Disconnecting flushOfflineStorage: $flushOfflineStorage; clearUser: $clearUser'); + + await _offlineStorage?.disconnect(flush: flushOfflineStorage); + _offlineStorage = null; + + if (clearUser == true) { + state.dispose(); + state = ClientState(this); + } + + await _disconnect(); + } + + Future _disconnect() async { + logger.info('Client disconnecting'); + + await _ws?.disconnect(); + } + + /// Requests users with a given query. + Future queryUsers({ + Map filter, + List sort, + Map options, + PaginationParams pagination, + }) async { + final defaultOptions = { + 'presence': _hasConnectionId, + }; + + final payload = { + 'filter_conditions': filter ?? {}, + 'sort': sort, + }; + + payload.addAll(defaultOptions); + + if (pagination != null) { + payload.addAll(pagination.toJson()); + } + + if (options != null) { + payload.addAll(options); + } + + final rawRes = await get( + '/users', + queryParameters: { + 'payload': jsonEncode(payload), + }, + ); + + final response = decode( + rawRes.data, + QueryUsersResponse.fromJson, + ); + + state?._updateUsers(response.users); + + return response; + } + + /// A message search. + Future search( + Map filters, + List sort, + String query, + PaginationParams paginationParams, { + Map messageFilters, + }) async { + final payload = { + 'filter_conditions': filters, + if (messageFilters != null) ...{ + 'message_filter_conditions': messageFilters, + }, + 'query': query, + 'sort': sort, + }; + + if (paginationParams != null) { + payload.addAll(paginationParams.toJson()); + } + + final response = await get('/search', + queryParameters: {'payload': json.encode(payload)}); + return decode( + response.data, SearchMessagesResponse.fromJson); + } + + /// Add a device for Push Notifications. + Future addDevice(String id, String pushProvider) async { + final response = await post('/devices', data: { + 'id': id, + 'push_provider': pushProvider, + }); + return decode(response.data, EmptyResponse.fromJson); + } + + /// Gets a list of user devices. + Future getDevices() async { + final response = await get('/devices'); + return decode( + response.data, ListDevicesResponse.fromJson); + } + + /// Remove a user's device. + Future removeDevice(String id) async { + final response = await delete('/devices', queryParameters: { + 'id': id, + }); + return decode(response.data, EmptyResponse.fromJson); + } + + /// Get a development token + String devToken(String userId) { + final payload = json.encode({'user_id': userId}); + final payloadBytes = utf8.encode(payload); + final payloadB64 = base64.encode(payloadBytes); + return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.$payloadB64.devtoken'; + } + + /// Returns a channel client with the given type, id and custom data. + Channel channel( + String type, { + String id, + Map extraData, + }) { + if (type != null && + id != null && + state.channels?.containsKey('$type:$id') == true) { + return state.channels['$type:$id']; + } + + return Channel(this, type, id, extraData); + } + + /// Update or Create the given user object. + Future updateUser(User user) async { + return updateUsers([user]); + } + + /// Batch update a list of users + Future updateUsers(List users) async { + final response = await post('/users', data: { + 'users': users.asMap().map((_, u) => MapEntry(u.id, u.toJson())), + }); + return decode( + response.data, + UpdateUsersResponse.fromJson, + ); + } + + /// Bans a user from all channels + Future banUser( + String targetUserID, [ + Map options = const {}, + ]) async { + final data = Map.from(options) + ..addAll({ + 'target_user_id': targetUserID, + }); + final response = await post( + '/moderation/ban', + data: data, + ); + return decode(response.data, EmptyResponse.fromJson); + } + + /// Remove global ban for a user + Future unbanUser( + String targetUserID, [ + Map options = const {}, + ]) async { + final data = Map.from(options) + ..addAll({ + 'target_user_id': targetUserID, + }); + final response = await delete( + '/moderation/ban', + queryParameters: data, + ); + return decode(response.data, EmptyResponse.fromJson); + } + + /// Shadow bans a user + Future shadowBan( + String targetID, [ + Map options = const {}, + ]) async { + return banUser(targetID, { + 'shadow': true, + ...options, + }); + } + + /// Removes shadow ban from a user + Future removeShadowBan( + String targetID, [ + Map options = const {}, + ]) async { + return unbanUser(targetID, { + 'shadow': true, + ...options, + }); + } + + /// Mutes a user + Future muteUser(String targetID) async { + final response = await post('/moderation/mute', data: { + 'target_id': targetID, + }); + return decode(response.data, EmptyResponse.fromJson); + } + + /// Unmutes a user + Future unmuteUser(String targetID) async { + final response = await post('/moderation/unmute', data: { + 'target_id': targetID, + }); + return decode(response.data, EmptyResponse.fromJson); + } + + /// Flag a message + Future flagMessage(String messageID) async { + final response = await post('/moderation/flag', data: { + 'target_message_id': messageID, + }); + return decode(response.data, EmptyResponse.fromJson); + } + + /// Unflag a message + Future unflagMessage(String messageId) async { + final response = await post('/moderation/unflag', data: { + 'target_message_id': messageId, + }); + return decode(response.data, EmptyResponse.fromJson); + } + + /// Flag a user + Future flagUser(String userId) async { + final response = await post('/moderation/flag', data: { + 'target_user_id': userId, + }); + return decode(response.data, EmptyResponse.fromJson); + } + + /// Unflag a message + Future unflagUser(String userId) async { + final response = await post('/moderation/unflag', data: { + 'target_user_id': userId, + }); + return decode(response.data, EmptyResponse.fromJson); + } + + /// Mark all channels for this user as read + Future markAllRead() async { + final response = await post('/channels/read'); + return decode(response.data, EmptyResponse.fromJson); + } + + /// Update the given message + Future updateMessage( + Message message, [ + String cid, + ]) async { + message = message.copyWith( + status: MessageSendingStatus.UPDATING, + updatedAt: message.updatedAt ?? DateTime.now(), + ); + + final channel = state?.channels != null ? state?.channels[cid] : null; + channel?.state?.addMessage(message); + + return post('/messages/${message.id}', data: {'message': message}) + .then((res) { + final updateMessageResponse = decode( + res?.data, + UpdateMessageResponse.fromJson, + ); + + channel?.state?.addMessage(updateMessageResponse?.message?.copyWith( + ownReactions: message.ownReactions, + )); + + return updateMessageResponse; + }).catchError((error) { + if (error is DioError && + error.type != DioErrorType.RESPONSE && + state?.channels != null) { + channel?.state?.retryQueue?.add([message]); + } + throw error; + }); + } + + /// Deletes the given message + Future deleteMessage(Message message, [String cid]) async { + if (message.status == MessageSendingStatus.FAILED) { + state.channels[cid].state.addMessage(message.copyWith( + type: 'deleted', + status: MessageSendingStatus.SENT, + )); + return EmptyResponse(); + } + + try { + message = message.copyWith( + type: 'deleted', + status: MessageSendingStatus.DELETING, + deletedAt: message.deletedAt ?? DateTime.now(), + ); + + if (state?.channels != null) { + state.channels[cid]?.state?.addMessage(message); + } + + final response = await delete('/messages/${message.id}'); + + if (state?.channels != null) { + state.channels[cid]?.state + ?.addMessage(message.copyWith(status: MessageSendingStatus.SENT)); + } + + return decode(response.data, EmptyResponse.fromJson); + } catch (error) { + if (error is DioError && + error.type != DioErrorType.RESPONSE && + state?.channels != null) { + state.channels[cid]?.state?.retryQueue?.add([message]); + } + rethrow; + } + } + + /// Get a message by id + Future getMessage(String messageId) async { + final response = await get('/messages/$messageId'); + return decode(response.data, GetMessageResponse.fromJson); + } +} + +/// The class that handles the state of the channel listening to the events +class ClientState { + final _subscriptions = []; + + /// Creates a new instance listening to events and updating the state + ClientState(this._client) { + _subscriptions.addAll([ + _client + .on() + .where((event) => event.me != null) + .map((e) => e.me) + .listen((user) { + _userController.add(user); + if (user.totalUnreadCount != null) { + _totalUnreadCountController.add(user.totalUnreadCount); + } + + if (user.unreadChannels != null) { + _unreadChannelsController.add(user.unreadChannels); + } + }), + _client + .on() + .where((event) => event.unreadChannels != null) + .map((e) => e.unreadChannels) + .listen((unreadChannels) { + _unreadChannelsController.add(unreadChannels); + }), + _client + .on() + .where((event) => event.totalUnreadCount != null) + .map((e) => e.totalUnreadCount) + .listen((totalUnreadCount) { + _totalUnreadCountController.add(totalUnreadCount); + }), + ]); + + _listenChannelDeleted(); + + _listenChannelHidden(); + + _listenUserUpdated(); + } + + void _listenChannelHidden() { + _subscriptions.add(_client.on(EventType.channelHidden).listen((event) { + _client._offlineStorage?.deleteChannels([event.cid]); + if (channels != null) { + channels = channels..removeWhere((cid, ch) => cid == event.cid); + } + })); + } + + void _listenUserUpdated() { + _subscriptions.add(_client.on(EventType.userUpdated).listen((event) { + if (event.user.id == user.id) { + user = OwnUser.fromJson(event.user.toJson()); + } + _updateUser(event.user); + })); + } + + void _listenChannelDeleted() { + _subscriptions.add(_client + .on( + EventType.channelDeleted, + EventType.notificationRemovedFromChannel, + EventType.notificationChannelDeleted, + ) + .listen((Event event) async { + final eventChannel = event.channel; + await _client._offlineStorage?.deleteChannels([eventChannel.cid]); + if (channels != null) { + channels = channels..remove(eventChannel.cid); + } + })); + } + + final Client _client; + + /// Update user information + set user(OwnUser user) { + _userController.add(user); + } + + void _updateUsers(List users) { + users?.forEach(_updateUser); + } + + void _updateUser(User user) { + final newUsers = { + ...users ?? {}, + user.id: user, + }; + _usersController.add(newUsers); + } + + /// The current user + OwnUser get user => _userController.value; + + /// The current user as a stream + Stream get userStream => _userController.stream; + + /// The current user + Map get users => _usersController.value; + + /// The current user as a stream + Stream> get usersStream => _usersController.stream; + + /// The current unread channels count + int get unreadChannels => _unreadChannelsController.value; + + /// The current unread channels count as a stream + Stream get unreadChannelsStream => _unreadChannelsController.stream; + + /// The current total unread messages count + int get totalUnreadCount => _totalUnreadCountController.value; + + /// The current total unread messages count as a stream + Stream get totalUnreadCountStream => _totalUnreadCountController.stream; + + /// The current list of channels in memory as a stream + Stream> get channelsStream => _channelsController.stream; + + /// The current list of channels in memory + Map get channels => _channelsController.value; + + set channels(Map v) { + _channelsController.add(v); + } + + final BehaviorSubject> _channelsController = + BehaviorSubject.seeded({}); + final BehaviorSubject _userController = BehaviorSubject(); + final BehaviorSubject> _usersController = + BehaviorSubject.seeded({}); + final BehaviorSubject _unreadChannelsController = BehaviorSubject(); + final BehaviorSubject _totalUnreadCountController = BehaviorSubject(); + + /// Call this method to dispose this object + void dispose() { + _subscriptions.forEach((s) => s.cancel()); + _userController.close(); + _unreadChannelsController.close(); + _totalUnreadCountController.close(); + channels.values.forEach((c) => c.dispose()); + _channelsController.close(); + } +} diff --git a/packages/dart_client/lib/src/db/models.part.dart b/packages/dart_client/lib/src/db/models.part.dart new file mode 100644 index 00000000..98fced53 --- /dev/null +++ b/packages/dart_client/lib/src/db/models.part.dart @@ -0,0 +1,258 @@ +part of 'offline_storage.dart'; + +@DataClassName('ChannelQuery') +class _ChannelQueries extends Table { + TextColumn get queryHash => text()(); + + TextColumn get channelCid => text()(); + + @override + Set get primaryKey => { + queryHash, + channelCid, + }; +} + +class _Channels extends Table { + TextColumn get id => text()(); + + TextColumn get type => text()(); + + TextColumn get cid => text()(); + + TextColumn get config => text()(); + + BoolColumn get frozen => boolean().withDefault(Constant(false))(); + + DateTimeColumn get lastMessageAt => dateTime().nullable()(); + + DateTimeColumn get createdAt => dateTime().nullable()(); + + DateTimeColumn get updatedAt => dateTime().nullable()(); + + DateTimeColumn get deletedAt => dateTime().nullable()(); + + IntColumn get memberCount => integer().nullable()(); + + TextColumn get extraData => text().nullable().map(_ExtraDataConverter())(); + + TextColumn get createdBy => text().nullable()(); + + @override + Set get primaryKey => {cid}; +} + +class _ConnectionEvent extends Table { + IntColumn get id => integer()(); + + TextColumn get ownUser => text().nullable().map(_ExtraDataConverter())(); + + IntColumn get totalUnreadCount => integer().nullable()(); + + IntColumn get unreadChannels => integer().nullable()(); + + DateTimeColumn get lastEventAt => dateTime().nullable()(); + + DateTimeColumn get lastSyncAt => dateTime().nullable()(); + + @override + Set get primaryKey => {id}; +} + +class _Users extends Table { + TextColumn get id => text()(); + + TextColumn get role => text().nullable()(); + + DateTimeColumn get createdAt => dateTime().nullable()(); + + DateTimeColumn get updatedAt => dateTime().nullable()(); + + DateTimeColumn get lastActive => dateTime().nullable()(); + + BoolColumn get online => boolean().nullable()(); + + BoolColumn get banned => boolean().nullable()(); + + TextColumn get extraData => text().nullable().map(_ExtraDataConverter())(); + + @override + Set get primaryKey => {id}; +} + +class _Reads extends Table { + DateTimeColumn get lastRead => dateTime()(); + + TextColumn get userId => text()(); + + TextColumn get channelCid => text()(); + + IntColumn get unreadMessages => integer().nullable()(); + + @override + Set get primaryKey => { + userId, + channelCid, + }; +} + +class _Reactions extends Table { + TextColumn get messageId => text()(); + + TextColumn get type => text()(); + + DateTimeColumn get createdAt => dateTime()(); + + IntColumn get score => integer().nullable()(); + + TextColumn get userId => text()(); + + TextColumn get extraData => text().nullable().map(_ExtraDataConverter())(); + + @override + Set get primaryKey => { + messageId, + type, + userId, + }; +} + +class _Messages extends Table { + TextColumn get id => text()(); + + TextColumn get messageText => text().nullable()(); + + TextColumn get attachmentJson => text().nullable()(); + + IntColumn get status => + integer().map(_MessageSendingStatusConverter()).nullable()(); + + TextColumn get type => text().nullable()(); + + List mentionedUsers; + + TextColumn get reactionCounts => + text().nullable().map(_ExtraDataConverter())(); + + TextColumn get reactionScores => + text().nullable().map(_ExtraDataConverter())(); + + TextColumn get parentId => text().nullable()(); + + TextColumn get quotedMessageId => text().nullable()(); + + IntColumn get replyCount => integer().nullable()(); + + BoolColumn get showInChannel => boolean().nullable()(); + + BoolColumn get shadowed => boolean().nullable()(); + + TextColumn get command => text().nullable()(); + + DateTimeColumn get createdAt => dateTime()(); + + DateTimeColumn get updatedAt => dateTime().nullable()(); + + DateTimeColumn get deletedAt => dateTime().nullable()(); + + TextColumn get userId => text().nullable()(); + + TextColumn get channelCid => text().nullable()(); + + TextColumn get extraData => text().nullable().map(_ExtraDataConverter())(); + + @override + Set get primaryKey => {id}; +} + +class _Members extends Table { + TextColumn get userId => text()(); + + TextColumn get channelCid => text()(); + + TextColumn get role => text().nullable()(); + + DateTimeColumn get inviteAcceptedAt => dateTime().nullable()(); + + DateTimeColumn get inviteRejectedAt => dateTime().nullable()(); + + BoolColumn get invited => boolean().nullable()(); + + BoolColumn get banned => boolean().nullable()(); + + BoolColumn get shadowBanned => boolean().nullable()(); + + BoolColumn get isModerator => boolean().nullable()(); + + DateTimeColumn get createdAt => dateTime()(); + + DateTimeColumn get updatedAt => dateTime().nullable()(); + + @override + Set get primaryKey => { + userId, + channelCid, + }; +} + +class _ExtraDataConverter extends TypeConverter, String> { + @override + Map mapToDart(fromDb) { + if (fromDb == null) { + return null; + } + return Map.from(jsonDecode(fromDb) ?? {}); + } + + @override + String mapToSql(value) { + return jsonEncode(value); + } +} + +class _MessageSendingStatusConverter + extends TypeConverter { + @override + MessageSendingStatus mapToDart(int fromDb) { + switch (fromDb) { + case 0: + return MessageSendingStatus.SENDING; + case 1: + return MessageSendingStatus.SENT; + case 2: + return MessageSendingStatus.FAILED; + case 3: + return MessageSendingStatus.UPDATING; + case 4: + return MessageSendingStatus.FAILED_UPDATE; + case 5: + return MessageSendingStatus.DELETING; + case 6: + return MessageSendingStatus.FAILED_DELETE; + default: + return null; + } + } + + @override + int mapToSql(MessageSendingStatus value) { + switch (value) { + case MessageSendingStatus.SENDING: + return 0; + case MessageSendingStatus.SENT: + return 1; + case MessageSendingStatus.FAILED: + return 2; + case MessageSendingStatus.UPDATING: + return 3; + case MessageSendingStatus.FAILED_UPDATE: + return 4; + case MessageSendingStatus.DELETING: + return 5; + case MessageSendingStatus.FAILED_DELETE: + return 6; + default: + return null; + } + } +} diff --git a/packages/dart_client/lib/src/db/offline_storage.dart b/packages/dart_client/lib/src/db/offline_storage.dart new file mode 100644 index 00000000..b132ae02 --- /dev/null +++ b/packages/dart_client/lib/src/db/offline_storage.dart @@ -0,0 +1,841 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart' show WidgetsFlutterBinding; +import 'package:logging/logging.dart'; +import 'package:moor/isolate.dart'; +import 'package:moor/moor.dart'; +import 'package:stream_chat/src/db/shared/shared_db.dart'; +import 'package:stream_chat/src/models/event.dart'; +import 'package:stream_chat/src/models/own_user.dart'; + +import '../api/requests.dart'; +import '../models/attachment.dart'; +import '../models/channel_config.dart'; +import '../models/channel_model.dart'; +import '../models/channel_state.dart'; +import '../models/member.dart'; +import '../models/message.dart'; +import '../models/reaction.dart'; +import '../models/read.dart'; +import '../models/user.dart'; + +part 'models.part.dart'; + +part 'offline_storage.g.dart'; + +/// Gets a new instance of the database running on a background isolate +Future connectDatabase(User user, Logger logger) async { + logger.info('Connecting on background isolate'); + WidgetsFlutterBinding.ensureInitialized(); + return SharedDB.constructOfflineStorage( + userId: user.id, + logger: logger, + ); +} + +LazyDatabase _openConnection(String userId) { + moorRuntimeOptions.dontWarnAboutMultipleDatabases = true; + return LazyDatabase(() async { + return await SharedDB.constructDatabase('db_$userId.sqlite'); + }); +} + +/// Offline database used for caching channel queries and state +@UseMoor(tables: [ + _ConnectionEvent, + _Channels, + _Users, + _Messages, + _Reads, + _Members, + _ChannelQueries, + _Reactions, +]) +class OfflineStorage extends _$OfflineStorage { + /// Creates a new database instance + OfflineStorage.connect( + DatabaseConnection connection, + this._userId, + this._isolate, + this._logger, + ) : super.connect(connection); + + /// Instantiate a new OfflineStorage + OfflineStorage( + this._userId, + this._logger, + ) : _isolate = null, + super(_openConnection(_userId)) { + _logger.info('Connecting on standard isolate'); + } + + final String _userId; + final MoorIsolate _isolate; + final Logger _logger; + + // you should bump this number whenever you change or add a table definition. Migrations + // are covered later in this readme. + @override + int get schemaVersion => 8; + + @override + MigrationStrategy get migration => MigrationStrategy( + onUpgrade: (openingDetails, before, after) async { + if (before != after) { + final m = createMigrator(); + for (final table in allTables) { + await m.deleteTable(table.actualTableName); + await m.createTable(table); + } + } + }, + ); + + /// Closes the database instance + /// If [flush] is true, the database data will be deleted + Future disconnect({bool flush = false}) async { + _logger.info('Disconnecting'); + if (flush) { + _logger.info('Flushing'); + await batch((batch) { + allTables.forEach((table) { + delete(table).go(); + }); + }); + } + + await _isolate?.shutdownAll(); + + await close(); + } + + /// Get stored replies by messageId + Future> getReplies( + String parentId, { + String lessThan, + }) async { + final offlineList = await Future.wait(await (select(messages).join([ + innerJoin(users, messages.userId.equalsExp(users.id)), + ]) + ..where(messages.parentId.equals(parentId)) + ..orderBy([ + OrderingTerm.asc(messages.createdAt), + ])) + .map(_messageFromJoinRow) + .get()); + if (lessThan != null) { + final lessThanIndex = offlineList.indexWhere((m) => m.id == lessThan); + offlineList.removeRange(lessThanIndex, offlineList.length); + } + + return offlineList; + } + + /// Get stored connection event + Future getConnectionInfo() async { + return select(connectionEvent).map((row) { + return Event( + me: row.ownUser != null ? OwnUser.fromJson(row.ownUser) : null, + totalUnreadCount: row.totalUnreadCount, + unreadChannels: row.unreadChannels, + ); + }).getSingle(); + } + + /// Get stored lastSyncAt + Future getLastSyncAt() async { + return select(connectionEvent).getSingle().then((r) => r?.lastSyncAt); + } + + /// Update stored connection event + Future updateConnectionInfo(Event event) async { + final connectionInfo = await select(connectionEvent).getSingle(); + + return into(connectionEvent).insert( + _ConnectionEventData( + id: 1, + lastSyncAt: connectionInfo?.lastSyncAt, + lastEventAt: event.createdAt ?? connectionInfo?.lastEventAt, + totalUnreadCount: + event.totalUnreadCount ?? connectionInfo?.totalUnreadCount, + ownUser: event.me?.toJson() ?? connectionInfo?.ownUser, + unreadChannels: event.unreadChannels ?? connectionInfo?.unreadChannels, + ), + mode: InsertMode.insertOrReplace, + ); + } + + /// Update stored lastSyncAt + Future updateLastSyncAt(DateTime lastSyncAt) async { + return await (update(connectionEvent)..where((r) => r.id.equals(1))).write( + _ConnectionEventCompanion( + id: Value(1), + lastSyncAt: Value(lastSyncAt), + ), + ); + } + + /// Get the channel cids saved in the offline storage + Future> getChannelCids() async { + return (select(channels) + ..orderBy([(c) => OrderingTerm.desc(c.lastMessageAt)]) + ..limit(250)) + .map((c) => c.cid) + .get(); + } + + /// Get channel data by cid + Future getChannel( + String cid, { + int limit, + String messageLessThan, + String messageGreaterThan, + }) async { + return await (select(channels)..where((c) => c.cid.equals(cid))).join([ + leftOuterJoin(users, channels.createdBy.equalsExp(users.id)), + ]).map((row) { + return _channelFromRow( + row.readTable(channels), + row.readTable(users), + limit: limit, + messageLessThan: messageLessThan, + messageGreaterThan: messageGreaterThan, + ); + }).getSingle(); + } + + /// Get list of channels by filter, sort and paginationParams + Future> getChannelStates({ + Map filter, + List sort = const [], + PaginationParams paginationParams, + }) async { + _logger.info('Get channel states'); + final hash = _computeHash(filter); + final cachedChannels = await Future.wait(await (select(channelQueries) + ..where((c) => c.queryHash.equals(hash))) + .get() + .then((channelQueries) { + final cids = channelQueries.map((c) => c.channelCid).toList(); + final query = select(channels)..where((c) => c.cid.isIn(cids)); + + sort = sort + ?.where((s) => ChannelModel.topLevelFields.contains(s.field)) + ?.toList(); + + if (sort != null && sort.isNotEmpty) { + query.orderBy(sort.map((s) { + final orderExpression = CustomExpression('channels.${s.field}'); + return (c) => OrderingTerm( + expression: orderExpression, + mode: s.direction == 1 ? OrderingMode.asc : OrderingMode.desc, + ); + }).toList()); + } + + if (paginationParams != null) { + query.limit( + paginationParams.limit ?? 10, + offset: paginationParams.offset, + ); + } + + return query.join([ + leftOuterJoin(users, channels.createdBy.equalsExp(users.id)), + ]).map((row) async { + final userRow = row.readTable(users); + final channelRow = row.readTable(channels); + + return _channelFromRow(channelRow, userRow); + }).get(); + })); + + _logger.info('Got ${cachedChannels.length} channels'); + + if (sort?.isEmpty != false && cachedChannels?.isNotEmpty == true) { + cachedChannels + .sort((a, b) => b.channel.updatedAt.compareTo(a.channel.updatedAt)); + cachedChannels.sort((a, b) { + final dateA = a.channel.lastMessageAt ?? a.channel.createdAt; + final dateB = b.channel.lastMessageAt ?? b.channel.createdAt; + return dateB.compareTo(dateA); + }); + } + + return cachedChannels; + } + + /// Update list of channel queries + /// If [clearQueryCache] is true before the insert + /// the list of matching rows will be deleted + Future updateChannelQueries( + Map filter, + List cids, + bool clearQueryCache, + ) async { + final hash = _computeHash(filter); + if (clearQueryCache) { + await (delete(channelQueries) + ..where( + (_ChannelQueries query) => query.queryHash.equals(hash), + )) + .go(); + } + + return batch((batch) { + batch.insertAll( + channelQueries, + cids.map((cid) { + return ChannelQuery( + queryHash: hash, + channelCid: cid, + ); + }).toList(), + mode: InsertMode.insertOrReplace, + ); + }); + } + + /// Remove a message by message id + Future deleteMessages(List messageIds) { + return batch((batch) { + batch.deleteWhere<_Reactions, _Reaction>( + reactions, + (r) => r.messageId.isIn(messageIds), + ); + batch.deleteWhere<_Messages, _Message>( + messages, + (m) => m.id.isIn(messageIds), + ); + }); + } + + /// Remove a message by message id + Future deleteChannelsMessages(List cids) async { + final messageIds = await (select(messages) + ..where((m) => m.channelCid.isIn(cids))) + .map((m) => m.id) + .get(); + return batch((batch) { + batch.deleteWhere<_Reactions, _Reaction>( + reactions, + (r) => r.messageId.isIn(messageIds), + ); + batch.deleteWhere<_Messages, _Message>( + messages, + (m) => m.id.isIn(messageIds), + ); + }); + } + + /// Remove a channel by cid + Future deleteChannels(List cids) async { + await deleteChannelsMessages(cids); + return batch((batch) { + batch.deleteWhere<_Members, _Member>( + members, + (m) => m.channelCid.isIn(cids), + ); + batch.deleteWhere<_Reads, _Read>( + reads, + (r) => r.channelCid.isIn(cids), + ); + batch.deleteWhere<_Channels, _Channel>( + channels, + (c) => c.cid.isIn(cids), + ); + }); + } + + /// Update messages data from a list + Future updateMessages( + List newMessages, + String cid, + ) { + return batch((batch) { + batch.insertAll( + messages, + newMessages.map( + (m) { + return _Message( + id: m.id, + attachmentJson: m.attachments != null + ? jsonEncode(m.attachments.map((a) => a.toJson()).toList()) + : null, + channelCid: cid, + type: m.type, + parentId: m.parentId, + quotedMessageId: m.quotedMessageId, + command: m.command, + createdAt: m.createdAt, + shadowed: m.shadowed, + showInChannel: m.showInChannel, + replyCount: m.replyCount, + reactionScores: m.reactionScores, + reactionCounts: m.reactionCounts, + status: m.status, + updatedAt: m.updatedAt, + extraData: m.extraData, + userId: m.user.id, + deletedAt: m.deletedAt, + messageText: m.text, + ); + }, + ).toList(), + mode: InsertMode.insertOrReplace, + ); + }); + } + + /// Update single channel state + Future updateChannelState(ChannelState channelState) async { + await updateChannelStates([channelState]); + } + + /// Update list of channel states + Future updateChannelStates(List channelStates) async { + channelStates.forEach((cs) { + updateMessages( + cs.messages, + cs.channel.cid, + ); + }); + + await batch((batch) { + _updateReactions(batch, channelStates); + + _updateUsers(batch, channelStates); + + _updateReads(channelStates, batch); + + _updateMembers(channelStates, batch); + + _updateChannels(batch, channelStates); + }); + } + + /// Get the info about channel threads + Future>> getChannelThreads(String cid) async { + final rowMessages = await Future.wait(await (select(messages).join([ + leftOuterJoin(users, messages.userId.equalsExp(users.id)), + ]) + ..where(messages.channelCid.equals(cid)) + ..where(isNotNull(messages.parentId)) + ..orderBy([ + OrderingTerm.asc(messages.createdAt), + ])) + .map(_messageFromJoinRow) + .get()); + + final threads = >{}; + rowMessages.forEach((message) { + if (threads.containsKey(message.parentId)) { + threads[message.parentId].add(message); + } else { + threads[message.parentId] = [message]; + } + }); + + return threads; + } + + Future _channelFromRow( + _Channel channelRow, + _User userRow, { + int limit, + String messageLessThan, + String messageGreaterThan, + }) async { + final rowMessages = await _getChannelMessages( + channelRow, + limit: limit, + lessThan: messageLessThan, + greaterThan: messageGreaterThan, + ); + final rowReads = await _getChannelReads(channelRow); + final rowMembers = await _getChannelMembers(channelRow); + + return ChannelState( + members: rowMembers, + read: rowReads, + messages: rowMessages, + channel: ChannelModel( + id: channelRow.id, + type: channelRow.type, + frozen: channelRow.frozen, + createdAt: channelRow.createdAt, + updatedAt: channelRow.updatedAt, + memberCount: channelRow.memberCount, + cid: channelRow.cid, + lastMessageAt: channelRow.lastMessageAt, + deletedAt: channelRow.deletedAt, + extraData: channelRow.extraData, + config: ChannelConfig.fromJson(jsonDecode(channelRow.config)), + createdBy: userRow != null ? _userFromUserRow(userRow) : null, + ), + ); + } + + String _computeHash(Map filter) { + if (filter == null) { + return 'allchannels'; + } + final hash = base64Encode(utf8.encode('filter: ${jsonEncode(filter)}')); + return hash; + } + + Future _getMessageById(String id) async { + if (id == null || id.isEmpty) return null; + final message = await Future.wait(await (select(messages).join([ + leftOuterJoin(users, messages.userId.equalsExp(users.id)), + ]) + ..where(messages.id.equals(id))) + .map(_messageFromJoinRow) + .get()); + return message.first; + } + + Future> _getChannelMessages( + _Channel channelRow, { + int limit, + String lessThan, + String greaterThan, + }) async { + final rowMessages = await Future.wait(await (select(messages).join([ + leftOuterJoin(users, messages.userId.equalsExp(users.id)), + ]) + ..where(messages.channelCid.equals(channelRow.cid)) + ..where( + isNull(messages.parentId) | messages.showInChannel.equals(true)) + ..orderBy([ + OrderingTerm.asc(messages.createdAt), + ])) + .map(_messageFromJoinRow) + .get()); + + if (lessThan != null) { + final lessThanIndex = rowMessages.indexWhere((m) => m.id == lessThan); + if (lessThanIndex != -1) { + rowMessages.removeRange(lessThanIndex, rowMessages.length); + } + } + if (greaterThan != null) { + final greaterThanIndex = + rowMessages.indexWhere((m) => m.id == greaterThan); + if (greaterThanIndex != -1) { + rowMessages.removeRange(0, greaterThanIndex); + } + } + if (limit != null) { + return rowMessages.take(limit).toList(); + } + return rowMessages; + } + + Future _messageFromJoinRow(row) async { + final messageRow = row.readTable(messages); + final userRow = row.readTable(users); + + final latestReactions = await _getLatestReactions(messageRow); + final ownReactions = await _getOwnReactions(messageRow); + final quotedMessage = await _getMessageById(messageRow.quotedMessageId); + + return Message( + shadowed: messageRow.shadowed, + latestReactions: latestReactions, + ownReactions: ownReactions, + attachments: messageRow.attachmentJson != null + ? List>.from( + jsonDecode(messageRow.attachmentJson)) + .map((j) => Attachment.fromJson(j)) + .toList() + : null, + createdAt: messageRow.createdAt, + extraData: messageRow.extraData, + updatedAt: messageRow.updatedAt, + id: messageRow.id, + type: messageRow.type, + status: messageRow.status, + command: messageRow.command, + parentId: messageRow.parentId, + quotedMessageId: messageRow.quotedMessageId, + quotedMessage: quotedMessage, + reactionCounts: messageRow.reactionCounts, + reactionScores: messageRow.reactionScores, + replyCount: messageRow.replyCount, + showInChannel: messageRow.showInChannel, + text: messageRow.messageText, + user: _userFromUserRow(userRow), + deletedAt: messageRow.deletedAt, + ); + } + + Future> _getLatestReactions(_Message messageRow) async { + return await (select(reactions).join([ + leftOuterJoin(users, reactions.userId.equalsExp(users.id)), + ]) + ..where(reactions.messageId.equals(messageRow.id)) + ..orderBy([OrderingTerm.asc(reactions.createdAt)])) + .map((row) { + final r = row.readTable(reactions); + final u = row.readTable(users); + return _reactionFromRow(r, u); + }).get(); + } + + Reaction _reactionFromRow(_Reaction r, _User u) { + return Reaction( + extraData: r.extraData, + type: r.type, + createdAt: r.createdAt, + userId: r.userId, + user: _userFromUserRow(u), + messageId: r.messageId, + score: r.score, + ); + } + + Future> _getOwnReactions(_Message messageRow) async { + return await (select(reactions).join([ + leftOuterJoin(users, reactions.userId.equalsExp(users.id)), + ]) + ..where(reactions.userId.equals(_userId)) + ..where(reactions.messageId.equals(messageRow.id)) + ..orderBy([OrderingTerm.asc(reactions.createdAt)])) + .map((row) { + final r = row.readTable(reactions); + final u = row.readTable(users); + return _reactionFromRow(r, u); + }).get(); + } + + Future> _getChannelReads(_Channel channelRow) async { + final rowReads = await (select(reads).join([ + leftOuterJoin(users, reads.userId.equalsExp(users.id)), + ]) + ..where(reads.channelCid.equals(channelRow.cid)) + ..orderBy([ + OrderingTerm.asc(reads.lastRead), + ])) + .map((row) { + final userRow = row.readTable(users); + final readRow = row.readTable(reads); + return Read( + user: _userFromUserRow(userRow), + lastRead: readRow.lastRead, + unreadMessages: readRow.unreadMessages, + ); + }).get(); + return rowReads; + } + + Future> _getChannelMembers(_Channel channelRow) async { + final rowMembers = await (select(members).join([ + leftOuterJoin(users, members.userId.equalsExp(users.id)), + ]) + ..where(members.channelCid.equals(channelRow.cid)) + ..orderBy([ + OrderingTerm.asc(members.createdAt), + ])) + .map((row) { + final userRow = row.readTable(users); + final memberRow = row.readTable(members); + return Member( + user: _userFromUserRow(userRow), + userId: userRow.id, + banned: memberRow.banned, + shadowBanned: memberRow.shadowBanned, + updatedAt: memberRow.updatedAt, + createdAt: memberRow.createdAt, + role: memberRow.role, + inviteAcceptedAt: memberRow.inviteAcceptedAt, + invited: memberRow.invited, + inviteRejectedAt: memberRow.inviteRejectedAt, + isModerator: memberRow.isModerator, + ); + }).get(); + return rowMembers; + } + + User _userFromUserRow(_User userRow) { + return User( + updatedAt: userRow.updatedAt, + role: userRow.role, + online: userRow.online, + lastActive: userRow.lastActive, + extraData: userRow.extraData, + banned: userRow.banned, + createdAt: userRow.createdAt, + id: userRow.id, + ); + } + + void _updateChannels(Batch batch, List channelStates) { + batch.insertAll( + channels, + channelStates.map((cs) { + final channel = cs.channel; + return _channelDataFromChannelModel(channel); + }).toList(), + mode: InsertMode.insertOrReplace, + ); + } + + void _updateMembers(List channelStates, Batch batch) async { + await (delete(members) + ..where((tbl) => + tbl.channelCid.isIn(channelStates.map((e) => e.channel.cid)))) + .go(); + final newMembers = channelStates + .map((cs) => cs.members.map((m) => _Member( + userId: m.user.id, + banned: m.banned, + shadowBanned: m.shadowBanned, + channelCid: cs.channel.cid, + createdAt: m.createdAt, + isModerator: m.isModerator, + inviteRejectedAt: m.inviteRejectedAt, + invited: m.invited, + inviteAcceptedAt: m.inviteAcceptedAt, + role: m.role, + updatedAt: m.updatedAt, + ))) + .where((v) => v != null) + .expand((v) => v); + if (newMembers != null && newMembers.isNotEmpty) { + batch.insertAll( + members, + newMembers.toList(), + mode: InsertMode.insertOrReplace, + ); + } + } + + void _updateReads(List channelStates, Batch batch) { + final newReads = channelStates + .map((cs) => cs.read?.map((r) => _Read( + lastRead: r.lastRead, + userId: r.user.id, + channelCid: cs.channel.cid, + unreadMessages: r.unreadMessages, + ))) + .where((v) => v != null) + .expand((v) => v); + + if (newReads != null && newReads.isNotEmpty) { + batch.insertAll( + reads, + newReads.toList(), + mode: InsertMode.insertOrReplace, + ); + } + } + + void _updateUsers(Batch batch, List channelStates) { + batch.insertAll( + users, + channelStates + .map((cs) => [ + if (cs.channel.createdBy != null) + _userDataFromUser(cs.channel.createdBy), + if (cs.messages != null) + ...cs.messages + .map((m) => [ + _userDataFromUser(m.user), + if (m.latestReactions != null) + ...m.latestReactions + .where((r) => r.user != null) + .map((r) => _userDataFromUser(r.user)), + if (m.ownReactions != null) + ...m.ownReactions + .where((r) => r.user != null) + .map((r) => _userDataFromUser(r.user)), + ]) + .expand((v) => v), + if (cs.read != null) + ...cs.read.map((r) => _userDataFromUser(r.user)), + if (cs.members != null) + ...cs.members.map((m) => _userDataFromUser(m.user)), + ]) + .expand((v) => v) + .toList(), + mode: InsertMode.insertOrReplace, + ); + } + + void _updateReactions(Batch batch, List channelStates) { + batch.deleteWhere<_Reactions, _Reaction>( + reactions, + (r) => r.messageId.isIn(channelStates + .map((cs) => cs.messages.map((m) => m.id)) + .expand((v) => v)), + ); + final newReactions = channelStates + .map((cs) => cs.messages.map((m) { + final ownReactions = + m.ownReactions?.where((e) => e.userId != null)?.map( + (r) => _reactionDataFromReaction(m, r), + ) ?? + []; + final latestReactions = + m.latestReactions?.where((e) => e.userId != null)?.map( + (r) => _reactionDataFromReaction(m, r), + ) ?? + []; + return [ + ...ownReactions, + ...latestReactions, + ]; + }).expand((v) => v)) + .expand((v) => v); + + if (newReactions.isNotEmpty) { + batch.insertAll( + reactions, + newReactions.toList(), + mode: InsertMode.insertOrReplace, + ); + } + } + + _Reaction _reactionDataFromReaction(Message m, Reaction r) { + return _Reaction( + messageId: m.id, + type: r.type, + extraData: r.extraData, + score: r.score, + createdAt: r.createdAt, + userId: r.userId, + ); + } + + _User _userDataFromUser(User user) { + return _User( + id: user.id, + createdAt: user.createdAt, + banned: user.banned, + extraData: user.extraData, + lastActive: user.lastActive, + online: user.online, + role: user.role, + updatedAt: user.updatedAt, + ); + } + + _Channel _channelDataFromChannelModel(ChannelModel channel) { + return _Channel( + id: channel.id, + config: jsonEncode(channel.config?.toJson() ?? {}), + type: channel.type, + frozen: channel.frozen, + createdAt: channel.createdAt, + updatedAt: channel.updatedAt, + memberCount: channel.memberCount, + cid: channel.cid, + lastMessageAt: channel.lastMessageAt, + deletedAt: channel.deletedAt, + extraData: channel.extraData, + createdBy: channel.createdBy?.id, + ); + } +} diff --git a/packages/dart_client/lib/src/db/offline_storage.g.dart b/packages/dart_client/lib/src/db/offline_storage.g.dart new file mode 100644 index 00000000..a87ecb92 --- /dev/null +++ b/packages/dart_client/lib/src/db/offline_storage.g.dart @@ -0,0 +1,4160 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'offline_storage.dart'; + +// ************************************************************************** +// MoorGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps, unnecessary_this +class _ConnectionEventData extends DataClass + implements Insertable<_ConnectionEventData> { + final int id; + final Map ownUser; + final int totalUnreadCount; + final int unreadChannels; + final DateTime lastEventAt; + final DateTime lastSyncAt; + _ConnectionEventData( + {@required this.id, + this.ownUser, + this.totalUnreadCount, + this.unreadChannels, + this.lastEventAt, + this.lastSyncAt}); + factory _ConnectionEventData.fromData( + Map data, GeneratedDatabase db, + {String prefix}) { + final effectivePrefix = prefix ?? ''; + final intType = db.typeSystem.forDartType(); + final stringType = db.typeSystem.forDartType(); + final dateTimeType = db.typeSystem.forDartType(); + return _ConnectionEventData( + id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']), + ownUser: $_ConnectionEventTable.$converter0.mapToDart(stringType + .mapFromDatabaseResponse(data['${effectivePrefix}own_user'])), + totalUnreadCount: intType.mapFromDatabaseResponse( + data['${effectivePrefix}total_unread_count']), + unreadChannels: intType + .mapFromDatabaseResponse(data['${effectivePrefix}unread_channels']), + lastEventAt: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}last_event_at']), + lastSyncAt: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}last_sync_at']), + ); + } + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (!nullToAbsent || id != null) { + map['id'] = Variable(id); + } + if (!nullToAbsent || ownUser != null) { + final converter = $_ConnectionEventTable.$converter0; + map['own_user'] = Variable(converter.mapToSql(ownUser)); + } + if (!nullToAbsent || totalUnreadCount != null) { + map['total_unread_count'] = Variable(totalUnreadCount); + } + if (!nullToAbsent || unreadChannels != null) { + map['unread_channels'] = Variable(unreadChannels); + } + if (!nullToAbsent || lastEventAt != null) { + map['last_event_at'] = Variable(lastEventAt); + } + if (!nullToAbsent || lastSyncAt != null) { + map['last_sync_at'] = Variable(lastSyncAt); + } + return map; + } + + _ConnectionEventCompanion toCompanion(bool nullToAbsent) { + return _ConnectionEventCompanion( + id: id == null && nullToAbsent ? const Value.absent() : Value(id), + ownUser: ownUser == null && nullToAbsent + ? const Value.absent() + : Value(ownUser), + totalUnreadCount: totalUnreadCount == null && nullToAbsent + ? const Value.absent() + : Value(totalUnreadCount), + unreadChannels: unreadChannels == null && nullToAbsent + ? const Value.absent() + : Value(unreadChannels), + lastEventAt: lastEventAt == null && nullToAbsent + ? const Value.absent() + : Value(lastEventAt), + lastSyncAt: lastSyncAt == null && nullToAbsent + ? const Value.absent() + : Value(lastSyncAt), + ); + } + + factory _ConnectionEventData.fromJson(Map json, + {ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return _ConnectionEventData( + id: serializer.fromJson(json['id']), + ownUser: serializer.fromJson>(json['ownUser']), + totalUnreadCount: serializer.fromJson(json['totalUnreadCount']), + unreadChannels: serializer.fromJson(json['unreadChannels']), + lastEventAt: serializer.fromJson(json['lastEventAt']), + lastSyncAt: serializer.fromJson(json['lastSyncAt']), + ); + } + @override + Map toJson({ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'ownUser': serializer.toJson>(ownUser), + 'totalUnreadCount': serializer.toJson(totalUnreadCount), + 'unreadChannels': serializer.toJson(unreadChannels), + 'lastEventAt': serializer.toJson(lastEventAt), + 'lastSyncAt': serializer.toJson(lastSyncAt), + }; + } + + _ConnectionEventData copyWith( + {int id, + Map ownUser, + int totalUnreadCount, + int unreadChannels, + DateTime lastEventAt, + DateTime lastSyncAt}) => + _ConnectionEventData( + id: id ?? this.id, + ownUser: ownUser ?? this.ownUser, + totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, + unreadChannels: unreadChannels ?? this.unreadChannels, + lastEventAt: lastEventAt ?? this.lastEventAt, + lastSyncAt: lastSyncAt ?? this.lastSyncAt, + ); + @override + String toString() { + return (StringBuffer('_ConnectionEventData(') + ..write('id: $id, ') + ..write('ownUser: $ownUser, ') + ..write('totalUnreadCount: $totalUnreadCount, ') + ..write('unreadChannels: $unreadChannels, ') + ..write('lastEventAt: $lastEventAt, ') + ..write('lastSyncAt: $lastSyncAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => $mrjf($mrjc( + id.hashCode, + $mrjc( + ownUser.hashCode, + $mrjc( + totalUnreadCount.hashCode, + $mrjc(unreadChannels.hashCode, + $mrjc(lastEventAt.hashCode, lastSyncAt.hashCode)))))); + @override + bool operator ==(dynamic other) => + identical(this, other) || + (other is _ConnectionEventData && + other.id == this.id && + other.ownUser == this.ownUser && + other.totalUnreadCount == this.totalUnreadCount && + other.unreadChannels == this.unreadChannels && + other.lastEventAt == this.lastEventAt && + other.lastSyncAt == this.lastSyncAt); +} + +class _ConnectionEventCompanion extends UpdateCompanion<_ConnectionEventData> { + final Value id; + final Value> ownUser; + final Value totalUnreadCount; + final Value unreadChannels; + final Value lastEventAt; + final Value lastSyncAt; + const _ConnectionEventCompanion({ + this.id = const Value.absent(), + this.ownUser = const Value.absent(), + this.totalUnreadCount = const Value.absent(), + this.unreadChannels = const Value.absent(), + this.lastEventAt = const Value.absent(), + this.lastSyncAt = const Value.absent(), + }); + _ConnectionEventCompanion.insert({ + this.id = const Value.absent(), + this.ownUser = const Value.absent(), + this.totalUnreadCount = const Value.absent(), + this.unreadChannels = const Value.absent(), + this.lastEventAt = const Value.absent(), + this.lastSyncAt = const Value.absent(), + }); + static Insertable<_ConnectionEventData> custom({ + Expression id, + Expression ownUser, + Expression totalUnreadCount, + Expression unreadChannels, + Expression lastEventAt, + Expression lastSyncAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (ownUser != null) 'own_user': ownUser, + if (totalUnreadCount != null) 'total_unread_count': totalUnreadCount, + if (unreadChannels != null) 'unread_channels': unreadChannels, + if (lastEventAt != null) 'last_event_at': lastEventAt, + if (lastSyncAt != null) 'last_sync_at': lastSyncAt, + }); + } + + _ConnectionEventCompanion copyWith( + {Value id, + Value> ownUser, + Value totalUnreadCount, + Value unreadChannels, + Value lastEventAt, + Value lastSyncAt}) { + return _ConnectionEventCompanion( + id: id ?? this.id, + ownUser: ownUser ?? this.ownUser, + totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, + unreadChannels: unreadChannels ?? this.unreadChannels, + lastEventAt: lastEventAt ?? this.lastEventAt, + lastSyncAt: lastSyncAt ?? this.lastSyncAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (ownUser.present) { + final converter = $_ConnectionEventTable.$converter0; + map['own_user'] = Variable(converter.mapToSql(ownUser.value)); + } + if (totalUnreadCount.present) { + map['total_unread_count'] = Variable(totalUnreadCount.value); + } + if (unreadChannels.present) { + map['unread_channels'] = Variable(unreadChannels.value); + } + if (lastEventAt.present) { + map['last_event_at'] = Variable(lastEventAt.value); + } + if (lastSyncAt.present) { + map['last_sync_at'] = Variable(lastSyncAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('_ConnectionEventCompanion(') + ..write('id: $id, ') + ..write('ownUser: $ownUser, ') + ..write('totalUnreadCount: $totalUnreadCount, ') + ..write('unreadChannels: $unreadChannels, ') + ..write('lastEventAt: $lastEventAt, ') + ..write('lastSyncAt: $lastSyncAt') + ..write(')')) + .toString(); + } +} + +class $_ConnectionEventTable extends _ConnectionEvent + with TableInfo<$_ConnectionEventTable, _ConnectionEventData> { + final GeneratedDatabase _db; + final String _alias; + $_ConnectionEventTable(this._db, [this._alias]); + final VerificationMeta _idMeta = const VerificationMeta('id'); + GeneratedIntColumn _id; + @override + GeneratedIntColumn get id => _id ??= _constructId(); + GeneratedIntColumn _constructId() { + return GeneratedIntColumn( + 'id', + $tableName, + false, + ); + } + + final VerificationMeta _ownUserMeta = const VerificationMeta('ownUser'); + GeneratedTextColumn _ownUser; + @override + GeneratedTextColumn get ownUser => _ownUser ??= _constructOwnUser(); + GeneratedTextColumn _constructOwnUser() { + return GeneratedTextColumn( + 'own_user', + $tableName, + true, + ); + } + + final VerificationMeta _totalUnreadCountMeta = + const VerificationMeta('totalUnreadCount'); + GeneratedIntColumn _totalUnreadCount; + @override + GeneratedIntColumn get totalUnreadCount => + _totalUnreadCount ??= _constructTotalUnreadCount(); + GeneratedIntColumn _constructTotalUnreadCount() { + return GeneratedIntColumn( + 'total_unread_count', + $tableName, + true, + ); + } + + final VerificationMeta _unreadChannelsMeta = + const VerificationMeta('unreadChannels'); + GeneratedIntColumn _unreadChannels; + @override + GeneratedIntColumn get unreadChannels => + _unreadChannels ??= _constructUnreadChannels(); + GeneratedIntColumn _constructUnreadChannels() { + return GeneratedIntColumn( + 'unread_channels', + $tableName, + true, + ); + } + + final VerificationMeta _lastEventAtMeta = + const VerificationMeta('lastEventAt'); + GeneratedDateTimeColumn _lastEventAt; + @override + GeneratedDateTimeColumn get lastEventAt => + _lastEventAt ??= _constructLastEventAt(); + GeneratedDateTimeColumn _constructLastEventAt() { + return GeneratedDateTimeColumn( + 'last_event_at', + $tableName, + true, + ); + } + + final VerificationMeta _lastSyncAtMeta = const VerificationMeta('lastSyncAt'); + GeneratedDateTimeColumn _lastSyncAt; + @override + GeneratedDateTimeColumn get lastSyncAt => + _lastSyncAt ??= _constructLastSyncAt(); + GeneratedDateTimeColumn _constructLastSyncAt() { + return GeneratedDateTimeColumn( + 'last_sync_at', + $tableName, + true, + ); + } + + @override + List get $columns => + [id, ownUser, totalUnreadCount, unreadChannels, lastEventAt, lastSyncAt]; + @override + $_ConnectionEventTable get asDslTable => this; + @override + String get $tableName => _alias ?? 'connection_event'; + @override + final String actualTableName = 'connection_event'; + @override + VerificationContext validateIntegrity( + Insertable<_ConnectionEventData> instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id'], _idMeta)); + } + context.handle(_ownUserMeta, const VerificationResult.success()); + if (data.containsKey('total_unread_count')) { + context.handle( + _totalUnreadCountMeta, + totalUnreadCount.isAcceptableOrUnknown( + data['total_unread_count'], _totalUnreadCountMeta)); + } + if (data.containsKey('unread_channels')) { + context.handle( + _unreadChannelsMeta, + unreadChannels.isAcceptableOrUnknown( + data['unread_channels'], _unreadChannelsMeta)); + } + if (data.containsKey('last_event_at')) { + context.handle( + _lastEventAtMeta, + lastEventAt.isAcceptableOrUnknown( + data['last_event_at'], _lastEventAtMeta)); + } + if (data.containsKey('last_sync_at')) { + context.handle( + _lastSyncAtMeta, + lastSyncAt.isAcceptableOrUnknown( + data['last_sync_at'], _lastSyncAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + _ConnectionEventData map(Map data, {String tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; + return _ConnectionEventData.fromData(data, _db, prefix: effectivePrefix); + } + + @override + $_ConnectionEventTable createAlias(String alias) { + return $_ConnectionEventTable(_db, alias); + } + + static TypeConverter, String> $converter0 = + _ExtraDataConverter(); +} + +class _Channel extends DataClass implements Insertable<_Channel> { + final String id; + final String type; + final String cid; + final String config; + final bool frozen; + final DateTime lastMessageAt; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime deletedAt; + final int memberCount; + final Map extraData; + final String createdBy; + _Channel( + {@required this.id, + @required this.type, + @required this.cid, + @required this.config, + @required this.frozen, + this.lastMessageAt, + this.createdAt, + this.updatedAt, + this.deletedAt, + this.memberCount, + this.extraData, + this.createdBy}); + factory _Channel.fromData(Map data, GeneratedDatabase db, + {String prefix}) { + final effectivePrefix = prefix ?? ''; + final stringType = db.typeSystem.forDartType(); + final boolType = db.typeSystem.forDartType(); + final dateTimeType = db.typeSystem.forDartType(); + final intType = db.typeSystem.forDartType(); + return _Channel( + id: stringType.mapFromDatabaseResponse(data['${effectivePrefix}id']), + type: stringType.mapFromDatabaseResponse(data['${effectivePrefix}type']), + cid: stringType.mapFromDatabaseResponse(data['${effectivePrefix}cid']), + config: + stringType.mapFromDatabaseResponse(data['${effectivePrefix}config']), + frozen: + boolType.mapFromDatabaseResponse(data['${effectivePrefix}frozen']), + lastMessageAt: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}last_message_at']), + createdAt: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}created_at']), + updatedAt: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}updated_at']), + deletedAt: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}deleted_at']), + memberCount: intType + .mapFromDatabaseResponse(data['${effectivePrefix}member_count']), + extraData: $_ChannelsTable.$converter0.mapToDart(stringType + .mapFromDatabaseResponse(data['${effectivePrefix}extra_data'])), + createdBy: stringType + .mapFromDatabaseResponse(data['${effectivePrefix}created_by']), + ); + } + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (!nullToAbsent || id != null) { + map['id'] = Variable(id); + } + if (!nullToAbsent || type != null) { + map['type'] = Variable(type); + } + if (!nullToAbsent || cid != null) { + map['cid'] = Variable(cid); + } + if (!nullToAbsent || config != null) { + map['config'] = Variable(config); + } + if (!nullToAbsent || frozen != null) { + map['frozen'] = Variable(frozen); + } + if (!nullToAbsent || lastMessageAt != null) { + map['last_message_at'] = Variable(lastMessageAt); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || updatedAt != null) { + map['updated_at'] = Variable(updatedAt); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || memberCount != null) { + map['member_count'] = Variable(memberCount); + } + if (!nullToAbsent || extraData != null) { + final converter = $_ChannelsTable.$converter0; + map['extra_data'] = Variable(converter.mapToSql(extraData)); + } + if (!nullToAbsent || createdBy != null) { + map['created_by'] = Variable(createdBy); + } + return map; + } + + _ChannelsCompanion toCompanion(bool nullToAbsent) { + return _ChannelsCompanion( + id: id == null && nullToAbsent ? const Value.absent() : Value(id), + type: type == null && nullToAbsent ? const Value.absent() : Value(type), + cid: cid == null && nullToAbsent ? const Value.absent() : Value(cid), + config: + config == null && nullToAbsent ? const Value.absent() : Value(config), + frozen: + frozen == null && nullToAbsent ? const Value.absent() : Value(frozen), + lastMessageAt: lastMessageAt == null && nullToAbsent + ? const Value.absent() + : Value(lastMessageAt), + createdAt: createdAt == null && nullToAbsent + ? const Value.absent() + : Value(createdAt), + updatedAt: updatedAt == null && nullToAbsent + ? const Value.absent() + : Value(updatedAt), + deletedAt: deletedAt == null && nullToAbsent + ? const Value.absent() + : Value(deletedAt), + memberCount: memberCount == null && nullToAbsent + ? const Value.absent() + : Value(memberCount), + extraData: extraData == null && nullToAbsent + ? const Value.absent() + : Value(extraData), + createdBy: createdBy == null && nullToAbsent + ? const Value.absent() + : Value(createdBy), + ); + } + + factory _Channel.fromJson(Map json, + {ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return _Channel( + id: serializer.fromJson(json['id']), + type: serializer.fromJson(json['type']), + cid: serializer.fromJson(json['cid']), + config: serializer.fromJson(json['config']), + frozen: serializer.fromJson(json['frozen']), + lastMessageAt: serializer.fromJson(json['lastMessageAt']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + memberCount: serializer.fromJson(json['memberCount']), + extraData: serializer.fromJson>(json['extraData']), + createdBy: serializer.fromJson(json['createdBy']), + ); + } + @override + Map toJson({ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'type': serializer.toJson(type), + 'cid': serializer.toJson(cid), + 'config': serializer.toJson(config), + 'frozen': serializer.toJson(frozen), + 'lastMessageAt': serializer.toJson(lastMessageAt), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'memberCount': serializer.toJson(memberCount), + 'extraData': serializer.toJson>(extraData), + 'createdBy': serializer.toJson(createdBy), + }; + } + + _Channel copyWith( + {String id, + String type, + String cid, + String config, + bool frozen, + DateTime lastMessageAt, + DateTime createdAt, + DateTime updatedAt, + DateTime deletedAt, + int memberCount, + Map extraData, + String createdBy}) => + _Channel( + id: id ?? this.id, + type: type ?? this.type, + cid: cid ?? this.cid, + config: config ?? this.config, + frozen: frozen ?? this.frozen, + lastMessageAt: lastMessageAt ?? this.lastMessageAt, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + memberCount: memberCount ?? this.memberCount, + extraData: extraData ?? this.extraData, + createdBy: createdBy ?? this.createdBy, + ); + @override + String toString() { + return (StringBuffer('_Channel(') + ..write('id: $id, ') + ..write('type: $type, ') + ..write('cid: $cid, ') + ..write('config: $config, ') + ..write('frozen: $frozen, ') + ..write('lastMessageAt: $lastMessageAt, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('memberCount: $memberCount, ') + ..write('extraData: $extraData, ') + ..write('createdBy: $createdBy') + ..write(')')) + .toString(); + } + + @override + int get hashCode => $mrjf($mrjc( + id.hashCode, + $mrjc( + type.hashCode, + $mrjc( + cid.hashCode, + $mrjc( + config.hashCode, + $mrjc( + frozen.hashCode, + $mrjc( + lastMessageAt.hashCode, + $mrjc( + createdAt.hashCode, + $mrjc( + updatedAt.hashCode, + $mrjc( + deletedAt.hashCode, + $mrjc( + memberCount.hashCode, + $mrjc(extraData.hashCode, + createdBy.hashCode)))))))))))); + @override + bool operator ==(dynamic other) => + identical(this, other) || + (other is _Channel && + other.id == this.id && + other.type == this.type && + other.cid == this.cid && + other.config == this.config && + other.frozen == this.frozen && + other.lastMessageAt == this.lastMessageAt && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.memberCount == this.memberCount && + other.extraData == this.extraData && + other.createdBy == this.createdBy); +} + +class _ChannelsCompanion extends UpdateCompanion<_Channel> { + final Value id; + final Value type; + final Value cid; + final Value config; + final Value frozen; + final Value lastMessageAt; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value memberCount; + final Value> extraData; + final Value createdBy; + const _ChannelsCompanion({ + this.id = const Value.absent(), + this.type = const Value.absent(), + this.cid = const Value.absent(), + this.config = const Value.absent(), + this.frozen = const Value.absent(), + this.lastMessageAt = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.memberCount = const Value.absent(), + this.extraData = const Value.absent(), + this.createdBy = const Value.absent(), + }); + _ChannelsCompanion.insert({ + @required String id, + @required String type, + @required String cid, + @required String config, + this.frozen = const Value.absent(), + this.lastMessageAt = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.memberCount = const Value.absent(), + this.extraData = const Value.absent(), + this.createdBy = const Value.absent(), + }) : id = Value(id), + type = Value(type), + cid = Value(cid), + config = Value(config); + static Insertable<_Channel> custom({ + Expression id, + Expression type, + Expression cid, + Expression config, + Expression frozen, + Expression lastMessageAt, + Expression createdAt, + Expression updatedAt, + Expression deletedAt, + Expression memberCount, + Expression extraData, + Expression createdBy, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (type != null) 'type': type, + if (cid != null) 'cid': cid, + if (config != null) 'config': config, + if (frozen != null) 'frozen': frozen, + if (lastMessageAt != null) 'last_message_at': lastMessageAt, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (memberCount != null) 'member_count': memberCount, + if (extraData != null) 'extra_data': extraData, + if (createdBy != null) 'created_by': createdBy, + }); + } + + _ChannelsCompanion copyWith( + {Value id, + Value type, + Value cid, + Value config, + Value frozen, + Value lastMessageAt, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value memberCount, + Value> extraData, + Value createdBy}) { + return _ChannelsCompanion( + id: id ?? this.id, + type: type ?? this.type, + cid: cid ?? this.cid, + config: config ?? this.config, + frozen: frozen ?? this.frozen, + lastMessageAt: lastMessageAt ?? this.lastMessageAt, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + memberCount: memberCount ?? this.memberCount, + extraData: extraData ?? this.extraData, + createdBy: createdBy ?? this.createdBy, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (cid.present) { + map['cid'] = Variable(cid.value); + } + if (config.present) { + map['config'] = Variable(config.value); + } + if (frozen.present) { + map['frozen'] = Variable(frozen.value); + } + if (lastMessageAt.present) { + map['last_message_at'] = Variable(lastMessageAt.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (memberCount.present) { + map['member_count'] = Variable(memberCount.value); + } + if (extraData.present) { + final converter = $_ChannelsTable.$converter0; + map['extra_data'] = Variable(converter.mapToSql(extraData.value)); + } + if (createdBy.present) { + map['created_by'] = Variable(createdBy.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('_ChannelsCompanion(') + ..write('id: $id, ') + ..write('type: $type, ') + ..write('cid: $cid, ') + ..write('config: $config, ') + ..write('frozen: $frozen, ') + ..write('lastMessageAt: $lastMessageAt, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('memberCount: $memberCount, ') + ..write('extraData: $extraData, ') + ..write('createdBy: $createdBy') + ..write(')')) + .toString(); + } +} + +class $_ChannelsTable extends _Channels + with TableInfo<$_ChannelsTable, _Channel> { + final GeneratedDatabase _db; + final String _alias; + $_ChannelsTable(this._db, [this._alias]); + final VerificationMeta _idMeta = const VerificationMeta('id'); + GeneratedTextColumn _id; + @override + GeneratedTextColumn get id => _id ??= _constructId(); + GeneratedTextColumn _constructId() { + return GeneratedTextColumn( + 'id', + $tableName, + false, + ); + } + + final VerificationMeta _typeMeta = const VerificationMeta('type'); + GeneratedTextColumn _type; + @override + GeneratedTextColumn get type => _type ??= _constructType(); + GeneratedTextColumn _constructType() { + return GeneratedTextColumn( + 'type', + $tableName, + false, + ); + } + + final VerificationMeta _cidMeta = const VerificationMeta('cid'); + GeneratedTextColumn _cid; + @override + GeneratedTextColumn get cid => _cid ??= _constructCid(); + GeneratedTextColumn _constructCid() { + return GeneratedTextColumn( + 'cid', + $tableName, + false, + ); + } + + final VerificationMeta _configMeta = const VerificationMeta('config'); + GeneratedTextColumn _config; + @override + GeneratedTextColumn get config => _config ??= _constructConfig(); + GeneratedTextColumn _constructConfig() { + return GeneratedTextColumn( + 'config', + $tableName, + false, + ); + } + + final VerificationMeta _frozenMeta = const VerificationMeta('frozen'); + GeneratedBoolColumn _frozen; + @override + GeneratedBoolColumn get frozen => _frozen ??= _constructFrozen(); + GeneratedBoolColumn _constructFrozen() { + return GeneratedBoolColumn('frozen', $tableName, false, + defaultValue: Constant(false)); + } + + final VerificationMeta _lastMessageAtMeta = + const VerificationMeta('lastMessageAt'); + GeneratedDateTimeColumn _lastMessageAt; + @override + GeneratedDateTimeColumn get lastMessageAt => + _lastMessageAt ??= _constructLastMessageAt(); + GeneratedDateTimeColumn _constructLastMessageAt() { + return GeneratedDateTimeColumn( + 'last_message_at', + $tableName, + true, + ); + } + + final VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); + GeneratedDateTimeColumn _createdAt; + @override + GeneratedDateTimeColumn get createdAt => _createdAt ??= _constructCreatedAt(); + GeneratedDateTimeColumn _constructCreatedAt() { + return GeneratedDateTimeColumn( + 'created_at', + $tableName, + true, + ); + } + + final VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); + GeneratedDateTimeColumn _updatedAt; + @override + GeneratedDateTimeColumn get updatedAt => _updatedAt ??= _constructUpdatedAt(); + GeneratedDateTimeColumn _constructUpdatedAt() { + return GeneratedDateTimeColumn( + 'updated_at', + $tableName, + true, + ); + } + + final VerificationMeta _deletedAtMeta = const VerificationMeta('deletedAt'); + GeneratedDateTimeColumn _deletedAt; + @override + GeneratedDateTimeColumn get deletedAt => _deletedAt ??= _constructDeletedAt(); + GeneratedDateTimeColumn _constructDeletedAt() { + return GeneratedDateTimeColumn( + 'deleted_at', + $tableName, + true, + ); + } + + final VerificationMeta _memberCountMeta = + const VerificationMeta('memberCount'); + GeneratedIntColumn _memberCount; + @override + GeneratedIntColumn get memberCount => + _memberCount ??= _constructMemberCount(); + GeneratedIntColumn _constructMemberCount() { + return GeneratedIntColumn( + 'member_count', + $tableName, + true, + ); + } + + final VerificationMeta _extraDataMeta = const VerificationMeta('extraData'); + GeneratedTextColumn _extraData; + @override + GeneratedTextColumn get extraData => _extraData ??= _constructExtraData(); + GeneratedTextColumn _constructExtraData() { + return GeneratedTextColumn( + 'extra_data', + $tableName, + true, + ); + } + + final VerificationMeta _createdByMeta = const VerificationMeta('createdBy'); + GeneratedTextColumn _createdBy; + @override + GeneratedTextColumn get createdBy => _createdBy ??= _constructCreatedBy(); + GeneratedTextColumn _constructCreatedBy() { + return GeneratedTextColumn( + 'created_by', + $tableName, + true, + ); + } + + @override + List get $columns => [ + id, + type, + cid, + config, + frozen, + lastMessageAt, + createdAt, + updatedAt, + deletedAt, + memberCount, + extraData, + createdBy + ]; + @override + $_ChannelsTable get asDslTable => this; + @override + String get $tableName => _alias ?? 'channels'; + @override + final String actualTableName = 'channels'; + @override + VerificationContext validateIntegrity(Insertable<_Channel> instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id'], _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('type')) { + context.handle( + _typeMeta, type.isAcceptableOrUnknown(data['type'], _typeMeta)); + } else if (isInserting) { + context.missing(_typeMeta); + } + if (data.containsKey('cid')) { + context.handle( + _cidMeta, cid.isAcceptableOrUnknown(data['cid'], _cidMeta)); + } else if (isInserting) { + context.missing(_cidMeta); + } + if (data.containsKey('config')) { + context.handle(_configMeta, + config.isAcceptableOrUnknown(data['config'], _configMeta)); + } else if (isInserting) { + context.missing(_configMeta); + } + if (data.containsKey('frozen')) { + context.handle(_frozenMeta, + frozen.isAcceptableOrUnknown(data['frozen'], _frozenMeta)); + } + if (data.containsKey('last_message_at')) { + context.handle( + _lastMessageAtMeta, + lastMessageAt.isAcceptableOrUnknown( + data['last_message_at'], _lastMessageAtMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at'], _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at'], _updatedAtMeta)); + } + if (data.containsKey('deleted_at')) { + context.handle(_deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at'], _deletedAtMeta)); + } + if (data.containsKey('member_count')) { + context.handle( + _memberCountMeta, + memberCount.isAcceptableOrUnknown( + data['member_count'], _memberCountMeta)); + } + context.handle(_extraDataMeta, const VerificationResult.success()); + if (data.containsKey('created_by')) { + context.handle(_createdByMeta, + createdBy.isAcceptableOrUnknown(data['created_by'], _createdByMeta)); + } + return context; + } + + @override + Set get $primaryKey => {cid}; + @override + _Channel map(Map data, {String tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; + return _Channel.fromData(data, _db, prefix: effectivePrefix); + } + + @override + $_ChannelsTable createAlias(String alias) { + return $_ChannelsTable(_db, alias); + } + + static TypeConverter, String> $converter0 = + _ExtraDataConverter(); +} + +class _User extends DataClass implements Insertable<_User> { + final String id; + final String role; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime lastActive; + final bool online; + final bool banned; + final Map extraData; + _User( + {@required this.id, + this.role, + this.createdAt, + this.updatedAt, + this.lastActive, + this.online, + this.banned, + this.extraData}); + factory _User.fromData(Map data, GeneratedDatabase db, + {String prefix}) { + final effectivePrefix = prefix ?? ''; + final stringType = db.typeSystem.forDartType(); + final dateTimeType = db.typeSystem.forDartType(); + final boolType = db.typeSystem.forDartType(); + return _User( + id: stringType.mapFromDatabaseResponse(data['${effectivePrefix}id']), + role: stringType.mapFromDatabaseResponse(data['${effectivePrefix}role']), + createdAt: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}created_at']), + updatedAt: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}updated_at']), + lastActive: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}last_active']), + online: + boolType.mapFromDatabaseResponse(data['${effectivePrefix}online']), + banned: + boolType.mapFromDatabaseResponse(data['${effectivePrefix}banned']), + extraData: $_UsersTable.$converter0.mapToDart(stringType + .mapFromDatabaseResponse(data['${effectivePrefix}extra_data'])), + ); + } + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (!nullToAbsent || id != null) { + map['id'] = Variable(id); + } + if (!nullToAbsent || role != null) { + map['role'] = Variable(role); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || updatedAt != null) { + map['updated_at'] = Variable(updatedAt); + } + if (!nullToAbsent || lastActive != null) { + map['last_active'] = Variable(lastActive); + } + if (!nullToAbsent || online != null) { + map['online'] = Variable(online); + } + if (!nullToAbsent || banned != null) { + map['banned'] = Variable(banned); + } + if (!nullToAbsent || extraData != null) { + final converter = $_UsersTable.$converter0; + map['extra_data'] = Variable(converter.mapToSql(extraData)); + } + return map; + } + + _UsersCompanion toCompanion(bool nullToAbsent) { + return _UsersCompanion( + id: id == null && nullToAbsent ? const Value.absent() : Value(id), + role: role == null && nullToAbsent ? const Value.absent() : Value(role), + createdAt: createdAt == null && nullToAbsent + ? const Value.absent() + : Value(createdAt), + updatedAt: updatedAt == null && nullToAbsent + ? const Value.absent() + : Value(updatedAt), + lastActive: lastActive == null && nullToAbsent + ? const Value.absent() + : Value(lastActive), + online: + online == null && nullToAbsent ? const Value.absent() : Value(online), + banned: + banned == null && nullToAbsent ? const Value.absent() : Value(banned), + extraData: extraData == null && nullToAbsent + ? const Value.absent() + : Value(extraData), + ); + } + + factory _User.fromJson(Map json, + {ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return _User( + id: serializer.fromJson(json['id']), + role: serializer.fromJson(json['role']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + lastActive: serializer.fromJson(json['lastActive']), + online: serializer.fromJson(json['online']), + banned: serializer.fromJson(json['banned']), + extraData: serializer.fromJson>(json['extraData']), + ); + } + @override + Map toJson({ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'role': serializer.toJson(role), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'lastActive': serializer.toJson(lastActive), + 'online': serializer.toJson(online), + 'banned': serializer.toJson(banned), + 'extraData': serializer.toJson>(extraData), + }; + } + + _User copyWith( + {String id, + String role, + DateTime createdAt, + DateTime updatedAt, + DateTime lastActive, + bool online, + bool banned, + Map extraData}) => + _User( + id: id ?? this.id, + role: role ?? this.role, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + lastActive: lastActive ?? this.lastActive, + online: online ?? this.online, + banned: banned ?? this.banned, + extraData: extraData ?? this.extraData, + ); + @override + String toString() { + return (StringBuffer('_User(') + ..write('id: $id, ') + ..write('role: $role, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('lastActive: $lastActive, ') + ..write('online: $online, ') + ..write('banned: $banned, ') + ..write('extraData: $extraData') + ..write(')')) + .toString(); + } + + @override + int get hashCode => $mrjf($mrjc( + id.hashCode, + $mrjc( + role.hashCode, + $mrjc( + createdAt.hashCode, + $mrjc( + updatedAt.hashCode, + $mrjc( + lastActive.hashCode, + $mrjc(online.hashCode, + $mrjc(banned.hashCode, extraData.hashCode)))))))); + @override + bool operator ==(dynamic other) => + identical(this, other) || + (other is _User && + other.id == this.id && + other.role == this.role && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.lastActive == this.lastActive && + other.online == this.online && + other.banned == this.banned && + other.extraData == this.extraData); +} + +class _UsersCompanion extends UpdateCompanion<_User> { + final Value id; + final Value role; + final Value createdAt; + final Value updatedAt; + final Value lastActive; + final Value online; + final Value banned; + final Value> extraData; + const _UsersCompanion({ + this.id = const Value.absent(), + this.role = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.lastActive = const Value.absent(), + this.online = const Value.absent(), + this.banned = const Value.absent(), + this.extraData = const Value.absent(), + }); + _UsersCompanion.insert({ + @required String id, + this.role = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.lastActive = const Value.absent(), + this.online = const Value.absent(), + this.banned = const Value.absent(), + this.extraData = const Value.absent(), + }) : id = Value(id); + static Insertable<_User> custom({ + Expression id, + Expression role, + Expression createdAt, + Expression updatedAt, + Expression lastActive, + Expression online, + Expression banned, + Expression extraData, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (role != null) 'role': role, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (lastActive != null) 'last_active': lastActive, + if (online != null) 'online': online, + if (banned != null) 'banned': banned, + if (extraData != null) 'extra_data': extraData, + }); + } + + _UsersCompanion copyWith( + {Value id, + Value role, + Value createdAt, + Value updatedAt, + Value lastActive, + Value online, + Value banned, + Value> extraData}) { + return _UsersCompanion( + id: id ?? this.id, + role: role ?? this.role, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + lastActive: lastActive ?? this.lastActive, + online: online ?? this.online, + banned: banned ?? this.banned, + extraData: extraData ?? this.extraData, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (lastActive.present) { + map['last_active'] = Variable(lastActive.value); + } + if (online.present) { + map['online'] = Variable(online.value); + } + if (banned.present) { + map['banned'] = Variable(banned.value); + } + if (extraData.present) { + final converter = $_UsersTable.$converter0; + map['extra_data'] = Variable(converter.mapToSql(extraData.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('_UsersCompanion(') + ..write('id: $id, ') + ..write('role: $role, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('lastActive: $lastActive, ') + ..write('online: $online, ') + ..write('banned: $banned, ') + ..write('extraData: $extraData') + ..write(')')) + .toString(); + } +} + +class $_UsersTable extends _Users with TableInfo<$_UsersTable, _User> { + final GeneratedDatabase _db; + final String _alias; + $_UsersTable(this._db, [this._alias]); + final VerificationMeta _idMeta = const VerificationMeta('id'); + GeneratedTextColumn _id; + @override + GeneratedTextColumn get id => _id ??= _constructId(); + GeneratedTextColumn _constructId() { + return GeneratedTextColumn( + 'id', + $tableName, + false, + ); + } + + final VerificationMeta _roleMeta = const VerificationMeta('role'); + GeneratedTextColumn _role; + @override + GeneratedTextColumn get role => _role ??= _constructRole(); + GeneratedTextColumn _constructRole() { + return GeneratedTextColumn( + 'role', + $tableName, + true, + ); + } + + final VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); + GeneratedDateTimeColumn _createdAt; + @override + GeneratedDateTimeColumn get createdAt => _createdAt ??= _constructCreatedAt(); + GeneratedDateTimeColumn _constructCreatedAt() { + return GeneratedDateTimeColumn( + 'created_at', + $tableName, + true, + ); + } + + final VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); + GeneratedDateTimeColumn _updatedAt; + @override + GeneratedDateTimeColumn get updatedAt => _updatedAt ??= _constructUpdatedAt(); + GeneratedDateTimeColumn _constructUpdatedAt() { + return GeneratedDateTimeColumn( + 'updated_at', + $tableName, + true, + ); + } + + final VerificationMeta _lastActiveMeta = const VerificationMeta('lastActive'); + GeneratedDateTimeColumn _lastActive; + @override + GeneratedDateTimeColumn get lastActive => + _lastActive ??= _constructLastActive(); + GeneratedDateTimeColumn _constructLastActive() { + return GeneratedDateTimeColumn( + 'last_active', + $tableName, + true, + ); + } + + final VerificationMeta _onlineMeta = const VerificationMeta('online'); + GeneratedBoolColumn _online; + @override + GeneratedBoolColumn get online => _online ??= _constructOnline(); + GeneratedBoolColumn _constructOnline() { + return GeneratedBoolColumn( + 'online', + $tableName, + true, + ); + } + + final VerificationMeta _bannedMeta = const VerificationMeta('banned'); + GeneratedBoolColumn _banned; + @override + GeneratedBoolColumn get banned => _banned ??= _constructBanned(); + GeneratedBoolColumn _constructBanned() { + return GeneratedBoolColumn( + 'banned', + $tableName, + true, + ); + } + + final VerificationMeta _extraDataMeta = const VerificationMeta('extraData'); + GeneratedTextColumn _extraData; + @override + GeneratedTextColumn get extraData => _extraData ??= _constructExtraData(); + GeneratedTextColumn _constructExtraData() { + return GeneratedTextColumn( + 'extra_data', + $tableName, + true, + ); + } + + @override + List get $columns => + [id, role, createdAt, updatedAt, lastActive, online, banned, extraData]; + @override + $_UsersTable get asDslTable => this; + @override + String get $tableName => _alias ?? 'users'; + @override + final String actualTableName = 'users'; + @override + VerificationContext validateIntegrity(Insertable<_User> instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id'], _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('role')) { + context.handle( + _roleMeta, role.isAcceptableOrUnknown(data['role'], _roleMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at'], _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at'], _updatedAtMeta)); + } + if (data.containsKey('last_active')) { + context.handle( + _lastActiveMeta, + lastActive.isAcceptableOrUnknown( + data['last_active'], _lastActiveMeta)); + } + if (data.containsKey('online')) { + context.handle(_onlineMeta, + online.isAcceptableOrUnknown(data['online'], _onlineMeta)); + } + if (data.containsKey('banned')) { + context.handle(_bannedMeta, + banned.isAcceptableOrUnknown(data['banned'], _bannedMeta)); + } + context.handle(_extraDataMeta, const VerificationResult.success()); + return context; + } + + @override + Set get $primaryKey => {id}; + @override + _User map(Map data, {String tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; + return _User.fromData(data, _db, prefix: effectivePrefix); + } + + @override + $_UsersTable createAlias(String alias) { + return $_UsersTable(_db, alias); + } + + static TypeConverter, String> $converter0 = + _ExtraDataConverter(); +} + +class _Message extends DataClass implements Insertable<_Message> { + final String id; + final String messageText; + final String attachmentJson; + final MessageSendingStatus status; + final String type; + final Map reactionCounts; + final Map reactionScores; + final String parentId; + final String quotedMessageId; + final int replyCount; + final bool showInChannel; + final bool shadowed; + final String command; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime deletedAt; + final String userId; + final String channelCid; + final Map extraData; + _Message( + {@required this.id, + this.messageText, + this.attachmentJson, + this.status, + this.type, + this.reactionCounts, + this.reactionScores, + this.parentId, + this.quotedMessageId, + this.replyCount, + this.showInChannel, + this.shadowed, + this.command, + @required this.createdAt, + this.updatedAt, + this.deletedAt, + this.userId, + this.channelCid, + this.extraData}); + factory _Message.fromData(Map data, GeneratedDatabase db, + {String prefix}) { + final effectivePrefix = prefix ?? ''; + final stringType = db.typeSystem.forDartType(); + final intType = db.typeSystem.forDartType(); + final boolType = db.typeSystem.forDartType(); + final dateTimeType = db.typeSystem.forDartType(); + return _Message( + id: stringType.mapFromDatabaseResponse(data['${effectivePrefix}id']), + messageText: stringType + .mapFromDatabaseResponse(data['${effectivePrefix}message_text']), + attachmentJson: stringType + .mapFromDatabaseResponse(data['${effectivePrefix}attachment_json']), + status: $_MessagesTable.$converter0.mapToDart( + intType.mapFromDatabaseResponse(data['${effectivePrefix}status'])), + type: stringType.mapFromDatabaseResponse(data['${effectivePrefix}type']), + reactionCounts: $_MessagesTable.$converter1.mapToDart(stringType + .mapFromDatabaseResponse(data['${effectivePrefix}reaction_counts'])), + reactionScores: $_MessagesTable.$converter2.mapToDart(stringType + .mapFromDatabaseResponse(data['${effectivePrefix}reaction_scores'])), + parentId: stringType + .mapFromDatabaseResponse(data['${effectivePrefix}parent_id']), + quotedMessageId: stringType + .mapFromDatabaseResponse(data['${effectivePrefix}quoted_message_id']), + replyCount: intType + .mapFromDatabaseResponse(data['${effectivePrefix}reply_count']), + showInChannel: boolType + .mapFromDatabaseResponse(data['${effectivePrefix}show_in_channel']), + shadowed: + boolType.mapFromDatabaseResponse(data['${effectivePrefix}shadowed']), + command: + stringType.mapFromDatabaseResponse(data['${effectivePrefix}command']), + createdAt: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}created_at']), + updatedAt: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}updated_at']), + deletedAt: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}deleted_at']), + userId: + stringType.mapFromDatabaseResponse(data['${effectivePrefix}user_id']), + channelCid: stringType + .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid']), + extraData: $_MessagesTable.$converter3.mapToDart(stringType + .mapFromDatabaseResponse(data['${effectivePrefix}extra_data'])), + ); + } + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (!nullToAbsent || id != null) { + map['id'] = Variable(id); + } + if (!nullToAbsent || messageText != null) { + map['message_text'] = Variable(messageText); + } + if (!nullToAbsent || attachmentJson != null) { + map['attachment_json'] = Variable(attachmentJson); + } + if (!nullToAbsent || status != null) { + final converter = $_MessagesTable.$converter0; + map['status'] = Variable(converter.mapToSql(status)); + } + if (!nullToAbsent || type != null) { + map['type'] = Variable(type); + } + if (!nullToAbsent || reactionCounts != null) { + final converter = $_MessagesTable.$converter1; + map['reaction_counts'] = + Variable(converter.mapToSql(reactionCounts)); + } + if (!nullToAbsent || reactionScores != null) { + final converter = $_MessagesTable.$converter2; + map['reaction_scores'] = + Variable(converter.mapToSql(reactionScores)); + } + if (!nullToAbsent || parentId != null) { + map['parent_id'] = Variable(parentId); + } + if (!nullToAbsent || quotedMessageId != null) { + map['quoted_message_id'] = Variable(quotedMessageId); + } + if (!nullToAbsent || replyCount != null) { + map['reply_count'] = Variable(replyCount); + } + if (!nullToAbsent || showInChannel != null) { + map['show_in_channel'] = Variable(showInChannel); + } + if (!nullToAbsent || shadowed != null) { + map['shadowed'] = Variable(shadowed); + } + if (!nullToAbsent || command != null) { + map['command'] = Variable(command); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || updatedAt != null) { + map['updated_at'] = Variable(updatedAt); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || userId != null) { + map['user_id'] = Variable(userId); + } + if (!nullToAbsent || channelCid != null) { + map['channel_cid'] = Variable(channelCid); + } + if (!nullToAbsent || extraData != null) { + final converter = $_MessagesTable.$converter3; + map['extra_data'] = Variable(converter.mapToSql(extraData)); + } + return map; + } + + _MessagesCompanion toCompanion(bool nullToAbsent) { + return _MessagesCompanion( + id: id == null && nullToAbsent ? const Value.absent() : Value(id), + messageText: messageText == null && nullToAbsent + ? const Value.absent() + : Value(messageText), + attachmentJson: attachmentJson == null && nullToAbsent + ? const Value.absent() + : Value(attachmentJson), + status: + status == null && nullToAbsent ? const Value.absent() : Value(status), + type: type == null && nullToAbsent ? const Value.absent() : Value(type), + reactionCounts: reactionCounts == null && nullToAbsent + ? const Value.absent() + : Value(reactionCounts), + reactionScores: reactionScores == null && nullToAbsent + ? const Value.absent() + : Value(reactionScores), + parentId: parentId == null && nullToAbsent + ? const Value.absent() + : Value(parentId), + quotedMessageId: quotedMessageId == null && nullToAbsent + ? const Value.absent() + : Value(quotedMessageId), + replyCount: replyCount == null && nullToAbsent + ? const Value.absent() + : Value(replyCount), + showInChannel: showInChannel == null && nullToAbsent + ? const Value.absent() + : Value(showInChannel), + shadowed: shadowed == null && nullToAbsent + ? const Value.absent() + : Value(shadowed), + command: command == null && nullToAbsent + ? const Value.absent() + : Value(command), + createdAt: createdAt == null && nullToAbsent + ? const Value.absent() + : Value(createdAt), + updatedAt: updatedAt == null && nullToAbsent + ? const Value.absent() + : Value(updatedAt), + deletedAt: deletedAt == null && nullToAbsent + ? const Value.absent() + : Value(deletedAt), + userId: + userId == null && nullToAbsent ? const Value.absent() : Value(userId), + channelCid: channelCid == null && nullToAbsent + ? const Value.absent() + : Value(channelCid), + extraData: extraData == null && nullToAbsent + ? const Value.absent() + : Value(extraData), + ); + } + + factory _Message.fromJson(Map json, + {ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return _Message( + id: serializer.fromJson(json['id']), + messageText: serializer.fromJson(json['messageText']), + attachmentJson: serializer.fromJson(json['attachmentJson']), + status: serializer.fromJson(json['status']), + type: serializer.fromJson(json['type']), + reactionCounts: + serializer.fromJson>(json['reactionCounts']), + reactionScores: + serializer.fromJson>(json['reactionScores']), + parentId: serializer.fromJson(json['parentId']), + quotedMessageId: serializer.fromJson(json['quotedMessageId']), + replyCount: serializer.fromJson(json['replyCount']), + showInChannel: serializer.fromJson(json['showInChannel']), + shadowed: serializer.fromJson(json['shadowed']), + command: serializer.fromJson(json['command']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + userId: serializer.fromJson(json['userId']), + channelCid: serializer.fromJson(json['channelCid']), + extraData: serializer.fromJson>(json['extraData']), + ); + } + @override + Map toJson({ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'messageText': serializer.toJson(messageText), + 'attachmentJson': serializer.toJson(attachmentJson), + 'status': serializer.toJson(status), + 'type': serializer.toJson(type), + 'reactionCounts': serializer.toJson>(reactionCounts), + 'reactionScores': serializer.toJson>(reactionScores), + 'parentId': serializer.toJson(parentId), + 'quotedMessageId': serializer.toJson(quotedMessageId), + 'replyCount': serializer.toJson(replyCount), + 'showInChannel': serializer.toJson(showInChannel), + 'shadowed': serializer.toJson(shadowed), + 'command': serializer.toJson(command), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'userId': serializer.toJson(userId), + 'channelCid': serializer.toJson(channelCid), + 'extraData': serializer.toJson>(extraData), + }; + } + + _Message copyWith( + {String id, + String messageText, + String attachmentJson, + MessageSendingStatus status, + String type, + Map reactionCounts, + Map reactionScores, + String parentId, + String quotedMessageId, + int replyCount, + bool showInChannel, + bool shadowed, + String command, + DateTime createdAt, + DateTime updatedAt, + DateTime deletedAt, + String userId, + String channelCid, + Map extraData}) => + _Message( + id: id ?? this.id, + messageText: messageText ?? this.messageText, + attachmentJson: attachmentJson ?? this.attachmentJson, + status: status ?? this.status, + type: type ?? this.type, + reactionCounts: reactionCounts ?? this.reactionCounts, + reactionScores: reactionScores ?? this.reactionScores, + parentId: parentId ?? this.parentId, + quotedMessageId: quotedMessageId ?? this.quotedMessageId, + replyCount: replyCount ?? this.replyCount, + showInChannel: showInChannel ?? this.showInChannel, + shadowed: shadowed ?? this.shadowed, + command: command ?? this.command, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + userId: userId ?? this.userId, + channelCid: channelCid ?? this.channelCid, + extraData: extraData ?? this.extraData, + ); + @override + String toString() { + return (StringBuffer('_Message(') + ..write('id: $id, ') + ..write('messageText: $messageText, ') + ..write('attachmentJson: $attachmentJson, ') + ..write('status: $status, ') + ..write('type: $type, ') + ..write('reactionCounts: $reactionCounts, ') + ..write('reactionScores: $reactionScores, ') + ..write('parentId: $parentId, ') + ..write('quotedMessageId: $quotedMessageId, ') + ..write('replyCount: $replyCount, ') + ..write('showInChannel: $showInChannel, ') + ..write('shadowed: $shadowed, ') + ..write('command: $command, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('userId: $userId, ') + ..write('channelCid: $channelCid, ') + ..write('extraData: $extraData') + ..write(')')) + .toString(); + } + + @override + int get hashCode => $mrjf($mrjc( + id.hashCode, + $mrjc( + messageText.hashCode, + $mrjc( + attachmentJson.hashCode, + $mrjc( + status.hashCode, + $mrjc( + type.hashCode, + $mrjc( + reactionCounts.hashCode, + $mrjc( + reactionScores.hashCode, + $mrjc( + parentId.hashCode, + $mrjc( + quotedMessageId.hashCode, + $mrjc( + replyCount.hashCode, + $mrjc( + showInChannel.hashCode, + $mrjc( + shadowed.hashCode, + $mrjc( + command.hashCode, + $mrjc( + createdAt.hashCode, + $mrjc( + updatedAt + .hashCode, + $mrjc( + deletedAt + .hashCode, + $mrjc( + userId + .hashCode, + $mrjc( + channelCid + .hashCode, + extraData + .hashCode))))))))))))))))))); + @override + bool operator ==(dynamic other) => + identical(this, other) || + (other is _Message && + other.id == this.id && + other.messageText == this.messageText && + other.attachmentJson == this.attachmentJson && + other.status == this.status && + other.type == this.type && + other.reactionCounts == this.reactionCounts && + other.reactionScores == this.reactionScores && + other.parentId == this.parentId && + other.quotedMessageId == this.quotedMessageId && + other.replyCount == this.replyCount && + other.showInChannel == this.showInChannel && + other.shadowed == this.shadowed && + other.command == this.command && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.userId == this.userId && + other.channelCid == this.channelCid && + other.extraData == this.extraData); +} + +class _MessagesCompanion extends UpdateCompanion<_Message> { + final Value id; + final Value messageText; + final Value attachmentJson; + final Value status; + final Value type; + final Value> reactionCounts; + final Value> reactionScores; + final Value parentId; + final Value quotedMessageId; + final Value replyCount; + final Value showInChannel; + final Value shadowed; + final Value command; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value userId; + final Value channelCid; + final Value> extraData; + const _MessagesCompanion({ + this.id = const Value.absent(), + this.messageText = const Value.absent(), + this.attachmentJson = const Value.absent(), + this.status = const Value.absent(), + this.type = const Value.absent(), + this.reactionCounts = const Value.absent(), + this.reactionScores = const Value.absent(), + this.parentId = const Value.absent(), + this.quotedMessageId = const Value.absent(), + this.replyCount = const Value.absent(), + this.showInChannel = const Value.absent(), + this.shadowed = const Value.absent(), + this.command = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.userId = const Value.absent(), + this.channelCid = const Value.absent(), + this.extraData = const Value.absent(), + }); + _MessagesCompanion.insert({ + @required String id, + this.messageText = const Value.absent(), + this.attachmentJson = const Value.absent(), + this.status = const Value.absent(), + this.type = const Value.absent(), + this.reactionCounts = const Value.absent(), + this.reactionScores = const Value.absent(), + this.parentId = const Value.absent(), + this.quotedMessageId = const Value.absent(), + this.replyCount = const Value.absent(), + this.showInChannel = const Value.absent(), + this.shadowed = const Value.absent(), + this.command = const Value.absent(), + @required DateTime createdAt, + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.userId = const Value.absent(), + this.channelCid = const Value.absent(), + this.extraData = const Value.absent(), + }) : id = Value(id), + createdAt = Value(createdAt); + static Insertable<_Message> custom({ + Expression id, + Expression messageText, + Expression attachmentJson, + Expression status, + Expression type, + Expression reactionCounts, + Expression reactionScores, + Expression parentId, + Expression quotedMessageId, + Expression replyCount, + Expression showInChannel, + Expression shadowed, + Expression command, + Expression createdAt, + Expression updatedAt, + Expression deletedAt, + Expression userId, + Expression channelCid, + Expression extraData, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (messageText != null) 'message_text': messageText, + if (attachmentJson != null) 'attachment_json': attachmentJson, + if (status != null) 'status': status, + if (type != null) 'type': type, + if (reactionCounts != null) 'reaction_counts': reactionCounts, + if (reactionScores != null) 'reaction_scores': reactionScores, + if (parentId != null) 'parent_id': parentId, + if (quotedMessageId != null) 'quoted_message_id': quotedMessageId, + if (replyCount != null) 'reply_count': replyCount, + if (showInChannel != null) 'show_in_channel': showInChannel, + if (shadowed != null) 'shadowed': shadowed, + if (command != null) 'command': command, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (userId != null) 'user_id': userId, + if (channelCid != null) 'channel_cid': channelCid, + if (extraData != null) 'extra_data': extraData, + }); + } + + _MessagesCompanion copyWith( + {Value id, + Value messageText, + Value attachmentJson, + Value status, + Value type, + Value> reactionCounts, + Value> reactionScores, + Value parentId, + Value quotedMessageId, + Value replyCount, + Value showInChannel, + Value shadowed, + Value command, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value userId, + Value channelCid, + Value> extraData}) { + return _MessagesCompanion( + id: id ?? this.id, + messageText: messageText ?? this.messageText, + attachmentJson: attachmentJson ?? this.attachmentJson, + status: status ?? this.status, + type: type ?? this.type, + reactionCounts: reactionCounts ?? this.reactionCounts, + reactionScores: reactionScores ?? this.reactionScores, + parentId: parentId ?? this.parentId, + quotedMessageId: quotedMessageId ?? this.quotedMessageId, + replyCount: replyCount ?? this.replyCount, + showInChannel: showInChannel ?? this.showInChannel, + shadowed: shadowed ?? this.shadowed, + command: command ?? this.command, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + userId: userId ?? this.userId, + channelCid: channelCid ?? this.channelCid, + extraData: extraData ?? this.extraData, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (messageText.present) { + map['message_text'] = Variable(messageText.value); + } + if (attachmentJson.present) { + map['attachment_json'] = Variable(attachmentJson.value); + } + if (status.present) { + final converter = $_MessagesTable.$converter0; + map['status'] = Variable(converter.mapToSql(status.value)); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (reactionCounts.present) { + final converter = $_MessagesTable.$converter1; + map['reaction_counts'] = + Variable(converter.mapToSql(reactionCounts.value)); + } + if (reactionScores.present) { + final converter = $_MessagesTable.$converter2; + map['reaction_scores'] = + Variable(converter.mapToSql(reactionScores.value)); + } + if (parentId.present) { + map['parent_id'] = Variable(parentId.value); + } + if (quotedMessageId.present) { + map['quoted_message_id'] = Variable(quotedMessageId.value); + } + if (replyCount.present) { + map['reply_count'] = Variable(replyCount.value); + } + if (showInChannel.present) { + map['show_in_channel'] = Variable(showInChannel.value); + } + if (shadowed.present) { + map['shadowed'] = Variable(shadowed.value); + } + if (command.present) { + map['command'] = Variable(command.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (channelCid.present) { + map['channel_cid'] = Variable(channelCid.value); + } + if (extraData.present) { + final converter = $_MessagesTable.$converter3; + map['extra_data'] = Variable(converter.mapToSql(extraData.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('_MessagesCompanion(') + ..write('id: $id, ') + ..write('messageText: $messageText, ') + ..write('attachmentJson: $attachmentJson, ') + ..write('status: $status, ') + ..write('type: $type, ') + ..write('reactionCounts: $reactionCounts, ') + ..write('reactionScores: $reactionScores, ') + ..write('parentId: $parentId, ') + ..write('quotedMessageId: $quotedMessageId, ') + ..write('replyCount: $replyCount, ') + ..write('showInChannel: $showInChannel, ') + ..write('shadowed: $shadowed, ') + ..write('command: $command, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('userId: $userId, ') + ..write('channelCid: $channelCid, ') + ..write('extraData: $extraData') + ..write(')')) + .toString(); + } +} + +class $_MessagesTable extends _Messages + with TableInfo<$_MessagesTable, _Message> { + final GeneratedDatabase _db; + final String _alias; + $_MessagesTable(this._db, [this._alias]); + final VerificationMeta _idMeta = const VerificationMeta('id'); + GeneratedTextColumn _id; + @override + GeneratedTextColumn get id => _id ??= _constructId(); + GeneratedTextColumn _constructId() { + return GeneratedTextColumn( + 'id', + $tableName, + false, + ); + } + + final VerificationMeta _messageTextMeta = + const VerificationMeta('messageText'); + GeneratedTextColumn _messageText; + @override + GeneratedTextColumn get messageText => + _messageText ??= _constructMessageText(); + GeneratedTextColumn _constructMessageText() { + return GeneratedTextColumn( + 'message_text', + $tableName, + true, + ); + } + + final VerificationMeta _attachmentJsonMeta = + const VerificationMeta('attachmentJson'); + GeneratedTextColumn _attachmentJson; + @override + GeneratedTextColumn get attachmentJson => + _attachmentJson ??= _constructAttachmentJson(); + GeneratedTextColumn _constructAttachmentJson() { + return GeneratedTextColumn( + 'attachment_json', + $tableName, + true, + ); + } + + final VerificationMeta _statusMeta = const VerificationMeta('status'); + GeneratedIntColumn _status; + @override + GeneratedIntColumn get status => _status ??= _constructStatus(); + GeneratedIntColumn _constructStatus() { + return GeneratedIntColumn( + 'status', + $tableName, + true, + ); + } + + final VerificationMeta _typeMeta = const VerificationMeta('type'); + GeneratedTextColumn _type; + @override + GeneratedTextColumn get type => _type ??= _constructType(); + GeneratedTextColumn _constructType() { + return GeneratedTextColumn( + 'type', + $tableName, + true, + ); + } + + final VerificationMeta _reactionCountsMeta = + const VerificationMeta('reactionCounts'); + GeneratedTextColumn _reactionCounts; + @override + GeneratedTextColumn get reactionCounts => + _reactionCounts ??= _constructReactionCounts(); + GeneratedTextColumn _constructReactionCounts() { + return GeneratedTextColumn( + 'reaction_counts', + $tableName, + true, + ); + } + + final VerificationMeta _reactionScoresMeta = + const VerificationMeta('reactionScores'); + GeneratedTextColumn _reactionScores; + @override + GeneratedTextColumn get reactionScores => + _reactionScores ??= _constructReactionScores(); + GeneratedTextColumn _constructReactionScores() { + return GeneratedTextColumn( + 'reaction_scores', + $tableName, + true, + ); + } + + final VerificationMeta _parentIdMeta = const VerificationMeta('parentId'); + GeneratedTextColumn _parentId; + @override + GeneratedTextColumn get parentId => _parentId ??= _constructParentId(); + GeneratedTextColumn _constructParentId() { + return GeneratedTextColumn( + 'parent_id', + $tableName, + true, + ); + } + + final VerificationMeta _quotedMessageIdMeta = + const VerificationMeta('quotedMessageId'); + GeneratedTextColumn _quotedMessageId; + @override + GeneratedTextColumn get quotedMessageId => + _quotedMessageId ??= _constructQuotedMessageId(); + GeneratedTextColumn _constructQuotedMessageId() { + return GeneratedTextColumn( + 'quoted_message_id', + $tableName, + true, + ); + } + + final VerificationMeta _replyCountMeta = const VerificationMeta('replyCount'); + GeneratedIntColumn _replyCount; + @override + GeneratedIntColumn get replyCount => _replyCount ??= _constructReplyCount(); + GeneratedIntColumn _constructReplyCount() { + return GeneratedIntColumn( + 'reply_count', + $tableName, + true, + ); + } + + final VerificationMeta _showInChannelMeta = + const VerificationMeta('showInChannel'); + GeneratedBoolColumn _showInChannel; + @override + GeneratedBoolColumn get showInChannel => + _showInChannel ??= _constructShowInChannel(); + GeneratedBoolColumn _constructShowInChannel() { + return GeneratedBoolColumn( + 'show_in_channel', + $tableName, + true, + ); + } + + final VerificationMeta _shadowedMeta = const VerificationMeta('shadowed'); + GeneratedBoolColumn _shadowed; + @override + GeneratedBoolColumn get shadowed => _shadowed ??= _constructShadowed(); + GeneratedBoolColumn _constructShadowed() { + return GeneratedBoolColumn( + 'shadowed', + $tableName, + true, + ); + } + + final VerificationMeta _commandMeta = const VerificationMeta('command'); + GeneratedTextColumn _command; + @override + GeneratedTextColumn get command => _command ??= _constructCommand(); + GeneratedTextColumn _constructCommand() { + return GeneratedTextColumn( + 'command', + $tableName, + true, + ); + } + + final VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); + GeneratedDateTimeColumn _createdAt; + @override + GeneratedDateTimeColumn get createdAt => _createdAt ??= _constructCreatedAt(); + GeneratedDateTimeColumn _constructCreatedAt() { + return GeneratedDateTimeColumn( + 'created_at', + $tableName, + false, + ); + } + + final VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); + GeneratedDateTimeColumn _updatedAt; + @override + GeneratedDateTimeColumn get updatedAt => _updatedAt ??= _constructUpdatedAt(); + GeneratedDateTimeColumn _constructUpdatedAt() { + return GeneratedDateTimeColumn( + 'updated_at', + $tableName, + true, + ); + } + + final VerificationMeta _deletedAtMeta = const VerificationMeta('deletedAt'); + GeneratedDateTimeColumn _deletedAt; + @override + GeneratedDateTimeColumn get deletedAt => _deletedAt ??= _constructDeletedAt(); + GeneratedDateTimeColumn _constructDeletedAt() { + return GeneratedDateTimeColumn( + 'deleted_at', + $tableName, + true, + ); + } + + final VerificationMeta _userIdMeta = const VerificationMeta('userId'); + GeneratedTextColumn _userId; + @override + GeneratedTextColumn get userId => _userId ??= _constructUserId(); + GeneratedTextColumn _constructUserId() { + return GeneratedTextColumn( + 'user_id', + $tableName, + true, + ); + } + + final VerificationMeta _channelCidMeta = const VerificationMeta('channelCid'); + GeneratedTextColumn _channelCid; + @override + GeneratedTextColumn get channelCid => _channelCid ??= _constructChannelCid(); + GeneratedTextColumn _constructChannelCid() { + return GeneratedTextColumn( + 'channel_cid', + $tableName, + true, + ); + } + + final VerificationMeta _extraDataMeta = const VerificationMeta('extraData'); + GeneratedTextColumn _extraData; + @override + GeneratedTextColumn get extraData => _extraData ??= _constructExtraData(); + GeneratedTextColumn _constructExtraData() { + return GeneratedTextColumn( + 'extra_data', + $tableName, + true, + ); + } + + @override + List get $columns => [ + id, + messageText, + attachmentJson, + status, + type, + reactionCounts, + reactionScores, + parentId, + quotedMessageId, + replyCount, + showInChannel, + shadowed, + command, + createdAt, + updatedAt, + deletedAt, + userId, + channelCid, + extraData + ]; + @override + $_MessagesTable get asDslTable => this; + @override + String get $tableName => _alias ?? 'messages'; + @override + final String actualTableName = 'messages'; + @override + VerificationContext validateIntegrity(Insertable<_Message> instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id'], _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('message_text')) { + context.handle( + _messageTextMeta, + messageText.isAcceptableOrUnknown( + data['message_text'], _messageTextMeta)); + } + if (data.containsKey('attachment_json')) { + context.handle( + _attachmentJsonMeta, + attachmentJson.isAcceptableOrUnknown( + data['attachment_json'], _attachmentJsonMeta)); + } + context.handle(_statusMeta, const VerificationResult.success()); + if (data.containsKey('type')) { + context.handle( + _typeMeta, type.isAcceptableOrUnknown(data['type'], _typeMeta)); + } + context.handle(_reactionCountsMeta, const VerificationResult.success()); + context.handle(_reactionScoresMeta, const VerificationResult.success()); + if (data.containsKey('parent_id')) { + context.handle(_parentIdMeta, + parentId.isAcceptableOrUnknown(data['parent_id'], _parentIdMeta)); + } + if (data.containsKey('quoted_message_id')) { + context.handle( + _quotedMessageIdMeta, + quotedMessageId.isAcceptableOrUnknown( + data['quoted_message_id'], _quotedMessageIdMeta)); + } + if (data.containsKey('reply_count')) { + context.handle( + _replyCountMeta, + replyCount.isAcceptableOrUnknown( + data['reply_count'], _replyCountMeta)); + } + if (data.containsKey('show_in_channel')) { + context.handle( + _showInChannelMeta, + showInChannel.isAcceptableOrUnknown( + data['show_in_channel'], _showInChannelMeta)); + } + if (data.containsKey('shadowed')) { + context.handle(_shadowedMeta, + shadowed.isAcceptableOrUnknown(data['shadowed'], _shadowedMeta)); + } + if (data.containsKey('command')) { + context.handle(_commandMeta, + command.isAcceptableOrUnknown(data['command'], _commandMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at'], _createdAtMeta)); + } else if (isInserting) { + context.missing(_createdAtMeta); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at'], _updatedAtMeta)); + } + if (data.containsKey('deleted_at')) { + context.handle(_deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at'], _deletedAtMeta)); + } + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id'], _userIdMeta)); + } + if (data.containsKey('channel_cid')) { + context.handle( + _channelCidMeta, + channelCid.isAcceptableOrUnknown( + data['channel_cid'], _channelCidMeta)); + } + context.handle(_extraDataMeta, const VerificationResult.success()); + return context; + } + + @override + Set get $primaryKey => {id}; + @override + _Message map(Map data, {String tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; + return _Message.fromData(data, _db, prefix: effectivePrefix); + } + + @override + $_MessagesTable createAlias(String alias) { + return $_MessagesTable(_db, alias); + } + + static TypeConverter $converter0 = + _MessageSendingStatusConverter(); + static TypeConverter, String> $converter1 = + _ExtraDataConverter(); + static TypeConverter, String> $converter2 = + _ExtraDataConverter(); + static TypeConverter, String> $converter3 = + _ExtraDataConverter(); +} + +class _Read extends DataClass implements Insertable<_Read> { + final DateTime lastRead; + final String userId; + final String channelCid; + final int unreadMessages; + _Read( + {@required this.lastRead, + @required this.userId, + @required this.channelCid, + this.unreadMessages}); + factory _Read.fromData(Map data, GeneratedDatabase db, + {String prefix}) { + final effectivePrefix = prefix ?? ''; + final dateTimeType = db.typeSystem.forDartType(); + final stringType = db.typeSystem.forDartType(); + final intType = db.typeSystem.forDartType(); + return _Read( + lastRead: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}last_read']), + userId: + stringType.mapFromDatabaseResponse(data['${effectivePrefix}user_id']), + channelCid: stringType + .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid']), + unreadMessages: intType + .mapFromDatabaseResponse(data['${effectivePrefix}unread_messages']), + ); + } + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (!nullToAbsent || lastRead != null) { + map['last_read'] = Variable(lastRead); + } + if (!nullToAbsent || userId != null) { + map['user_id'] = Variable(userId); + } + if (!nullToAbsent || channelCid != null) { + map['channel_cid'] = Variable(channelCid); + } + if (!nullToAbsent || unreadMessages != null) { + map['unread_messages'] = Variable(unreadMessages); + } + return map; + } + + _ReadsCompanion toCompanion(bool nullToAbsent) { + return _ReadsCompanion( + lastRead: lastRead == null && nullToAbsent + ? const Value.absent() + : Value(lastRead), + userId: + userId == null && nullToAbsent ? const Value.absent() : Value(userId), + channelCid: channelCid == null && nullToAbsent + ? const Value.absent() + : Value(channelCid), + unreadMessages: unreadMessages == null && nullToAbsent + ? const Value.absent() + : Value(unreadMessages), + ); + } + + factory _Read.fromJson(Map json, + {ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return _Read( + lastRead: serializer.fromJson(json['lastRead']), + userId: serializer.fromJson(json['userId']), + channelCid: serializer.fromJson(json['channelCid']), + unreadMessages: serializer.fromJson(json['unreadMessages']), + ); + } + @override + Map toJson({ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return { + 'lastRead': serializer.toJson(lastRead), + 'userId': serializer.toJson(userId), + 'channelCid': serializer.toJson(channelCid), + 'unreadMessages': serializer.toJson(unreadMessages), + }; + } + + _Read copyWith( + {DateTime lastRead, + String userId, + String channelCid, + int unreadMessages}) => + _Read( + lastRead: lastRead ?? this.lastRead, + userId: userId ?? this.userId, + channelCid: channelCid ?? this.channelCid, + unreadMessages: unreadMessages ?? this.unreadMessages, + ); + @override + String toString() { + return (StringBuffer('_Read(') + ..write('lastRead: $lastRead, ') + ..write('userId: $userId, ') + ..write('channelCid: $channelCid, ') + ..write('unreadMessages: $unreadMessages') + ..write(')')) + .toString(); + } + + @override + int get hashCode => $mrjf($mrjc( + lastRead.hashCode, + $mrjc(userId.hashCode, + $mrjc(channelCid.hashCode, unreadMessages.hashCode)))); + @override + bool operator ==(dynamic other) => + identical(this, other) || + (other is _Read && + other.lastRead == this.lastRead && + other.userId == this.userId && + other.channelCid == this.channelCid && + other.unreadMessages == this.unreadMessages); +} + +class _ReadsCompanion extends UpdateCompanion<_Read> { + final Value lastRead; + final Value userId; + final Value channelCid; + final Value unreadMessages; + const _ReadsCompanion({ + this.lastRead = const Value.absent(), + this.userId = const Value.absent(), + this.channelCid = const Value.absent(), + this.unreadMessages = const Value.absent(), + }); + _ReadsCompanion.insert({ + @required DateTime lastRead, + @required String userId, + @required String channelCid, + this.unreadMessages = const Value.absent(), + }) : lastRead = Value(lastRead), + userId = Value(userId), + channelCid = Value(channelCid); + static Insertable<_Read> custom({ + Expression lastRead, + Expression userId, + Expression channelCid, + Expression unreadMessages, + }) { + return RawValuesInsertable({ + if (lastRead != null) 'last_read': lastRead, + if (userId != null) 'user_id': userId, + if (channelCid != null) 'channel_cid': channelCid, + if (unreadMessages != null) 'unread_messages': unreadMessages, + }); + } + + _ReadsCompanion copyWith( + {Value lastRead, + Value userId, + Value channelCid, + Value unreadMessages}) { + return _ReadsCompanion( + lastRead: lastRead ?? this.lastRead, + userId: userId ?? this.userId, + channelCid: channelCid ?? this.channelCid, + unreadMessages: unreadMessages ?? this.unreadMessages, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (lastRead.present) { + map['last_read'] = Variable(lastRead.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (channelCid.present) { + map['channel_cid'] = Variable(channelCid.value); + } + if (unreadMessages.present) { + map['unread_messages'] = Variable(unreadMessages.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('_ReadsCompanion(') + ..write('lastRead: $lastRead, ') + ..write('userId: $userId, ') + ..write('channelCid: $channelCid, ') + ..write('unreadMessages: $unreadMessages') + ..write(')')) + .toString(); + } +} + +class $_ReadsTable extends _Reads with TableInfo<$_ReadsTable, _Read> { + final GeneratedDatabase _db; + final String _alias; + $_ReadsTable(this._db, [this._alias]); + final VerificationMeta _lastReadMeta = const VerificationMeta('lastRead'); + GeneratedDateTimeColumn _lastRead; + @override + GeneratedDateTimeColumn get lastRead => _lastRead ??= _constructLastRead(); + GeneratedDateTimeColumn _constructLastRead() { + return GeneratedDateTimeColumn( + 'last_read', + $tableName, + false, + ); + } + + final VerificationMeta _userIdMeta = const VerificationMeta('userId'); + GeneratedTextColumn _userId; + @override + GeneratedTextColumn get userId => _userId ??= _constructUserId(); + GeneratedTextColumn _constructUserId() { + return GeneratedTextColumn( + 'user_id', + $tableName, + false, + ); + } + + final VerificationMeta _channelCidMeta = const VerificationMeta('channelCid'); + GeneratedTextColumn _channelCid; + @override + GeneratedTextColumn get channelCid => _channelCid ??= _constructChannelCid(); + GeneratedTextColumn _constructChannelCid() { + return GeneratedTextColumn( + 'channel_cid', + $tableName, + false, + ); + } + + final VerificationMeta _unreadMessagesMeta = + const VerificationMeta('unreadMessages'); + GeneratedIntColumn _unreadMessages; + @override + GeneratedIntColumn get unreadMessages => + _unreadMessages ??= _constructUnreadMessages(); + GeneratedIntColumn _constructUnreadMessages() { + return GeneratedIntColumn( + 'unread_messages', + $tableName, + true, + ); + } + + @override + List get $columns => + [lastRead, userId, channelCid, unreadMessages]; + @override + $_ReadsTable get asDslTable => this; + @override + String get $tableName => _alias ?? 'reads'; + @override + final String actualTableName = 'reads'; + @override + VerificationContext validateIntegrity(Insertable<_Read> instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('last_read')) { + context.handle(_lastReadMeta, + lastRead.isAcceptableOrUnknown(data['last_read'], _lastReadMeta)); + } else if (isInserting) { + context.missing(_lastReadMeta); + } + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id'], _userIdMeta)); + } else if (isInserting) { + context.missing(_userIdMeta); + } + if (data.containsKey('channel_cid')) { + context.handle( + _channelCidMeta, + channelCid.isAcceptableOrUnknown( + data['channel_cid'], _channelCidMeta)); + } else if (isInserting) { + context.missing(_channelCidMeta); + } + if (data.containsKey('unread_messages')) { + context.handle( + _unreadMessagesMeta, + unreadMessages.isAcceptableOrUnknown( + data['unread_messages'], _unreadMessagesMeta)); + } + return context; + } + + @override + Set get $primaryKey => {userId, channelCid}; + @override + _Read map(Map data, {String tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; + return _Read.fromData(data, _db, prefix: effectivePrefix); + } + + @override + $_ReadsTable createAlias(String alias) { + return $_ReadsTable(_db, alias); + } +} + +class _Member extends DataClass implements Insertable<_Member> { + final String userId; + final String channelCid; + final String role; + final DateTime inviteAcceptedAt; + final DateTime inviteRejectedAt; + final bool invited; + final bool banned; + final bool shadowBanned; + final bool isModerator; + final DateTime createdAt; + final DateTime updatedAt; + _Member( + {@required this.userId, + @required this.channelCid, + this.role, + this.inviteAcceptedAt, + this.inviteRejectedAt, + this.invited, + this.banned, + this.shadowBanned, + this.isModerator, + @required this.createdAt, + this.updatedAt}); + factory _Member.fromData(Map data, GeneratedDatabase db, + {String prefix}) { + final effectivePrefix = prefix ?? ''; + final stringType = db.typeSystem.forDartType(); + final dateTimeType = db.typeSystem.forDartType(); + final boolType = db.typeSystem.forDartType(); + return _Member( + userId: + stringType.mapFromDatabaseResponse(data['${effectivePrefix}user_id']), + channelCid: stringType + .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid']), + role: stringType.mapFromDatabaseResponse(data['${effectivePrefix}role']), + inviteAcceptedAt: dateTimeType.mapFromDatabaseResponse( + data['${effectivePrefix}invite_accepted_at']), + inviteRejectedAt: dateTimeType.mapFromDatabaseResponse( + data['${effectivePrefix}invite_rejected_at']), + invited: + boolType.mapFromDatabaseResponse(data['${effectivePrefix}invited']), + banned: + boolType.mapFromDatabaseResponse(data['${effectivePrefix}banned']), + shadowBanned: boolType + .mapFromDatabaseResponse(data['${effectivePrefix}shadow_banned']), + isModerator: boolType + .mapFromDatabaseResponse(data['${effectivePrefix}is_moderator']), + createdAt: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}created_at']), + updatedAt: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}updated_at']), + ); + } + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (!nullToAbsent || userId != null) { + map['user_id'] = Variable(userId); + } + if (!nullToAbsent || channelCid != null) { + map['channel_cid'] = Variable(channelCid); + } + if (!nullToAbsent || role != null) { + map['role'] = Variable(role); + } + if (!nullToAbsent || inviteAcceptedAt != null) { + map['invite_accepted_at'] = Variable(inviteAcceptedAt); + } + if (!nullToAbsent || inviteRejectedAt != null) { + map['invite_rejected_at'] = Variable(inviteRejectedAt); + } + if (!nullToAbsent || invited != null) { + map['invited'] = Variable(invited); + } + if (!nullToAbsent || banned != null) { + map['banned'] = Variable(banned); + } + if (!nullToAbsent || shadowBanned != null) { + map['shadow_banned'] = Variable(shadowBanned); + } + if (!nullToAbsent || isModerator != null) { + map['is_moderator'] = Variable(isModerator); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || updatedAt != null) { + map['updated_at'] = Variable(updatedAt); + } + return map; + } + + _MembersCompanion toCompanion(bool nullToAbsent) { + return _MembersCompanion( + userId: + userId == null && nullToAbsent ? const Value.absent() : Value(userId), + channelCid: channelCid == null && nullToAbsent + ? const Value.absent() + : Value(channelCid), + role: role == null && nullToAbsent ? const Value.absent() : Value(role), + inviteAcceptedAt: inviteAcceptedAt == null && nullToAbsent + ? const Value.absent() + : Value(inviteAcceptedAt), + inviteRejectedAt: inviteRejectedAt == null && nullToAbsent + ? const Value.absent() + : Value(inviteRejectedAt), + invited: invited == null && nullToAbsent + ? const Value.absent() + : Value(invited), + banned: + banned == null && nullToAbsent ? const Value.absent() : Value(banned), + shadowBanned: shadowBanned == null && nullToAbsent + ? const Value.absent() + : Value(shadowBanned), + isModerator: isModerator == null && nullToAbsent + ? const Value.absent() + : Value(isModerator), + createdAt: createdAt == null && nullToAbsent + ? const Value.absent() + : Value(createdAt), + updatedAt: updatedAt == null && nullToAbsent + ? const Value.absent() + : Value(updatedAt), + ); + } + + factory _Member.fromJson(Map json, + {ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return _Member( + userId: serializer.fromJson(json['userId']), + channelCid: serializer.fromJson(json['channelCid']), + role: serializer.fromJson(json['role']), + inviteAcceptedAt: serializer.fromJson(json['inviteAcceptedAt']), + inviteRejectedAt: serializer.fromJson(json['inviteRejectedAt']), + invited: serializer.fromJson(json['invited']), + banned: serializer.fromJson(json['banned']), + shadowBanned: serializer.fromJson(json['shadowBanned']), + isModerator: serializer.fromJson(json['isModerator']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'channelCid': serializer.toJson(channelCid), + 'role': serializer.toJson(role), + 'inviteAcceptedAt': serializer.toJson(inviteAcceptedAt), + 'inviteRejectedAt': serializer.toJson(inviteRejectedAt), + 'invited': serializer.toJson(invited), + 'banned': serializer.toJson(banned), + 'shadowBanned': serializer.toJson(shadowBanned), + 'isModerator': serializer.toJson(isModerator), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } + + _Member copyWith( + {String userId, + String channelCid, + String role, + DateTime inviteAcceptedAt, + DateTime inviteRejectedAt, + bool invited, + bool banned, + bool shadowBanned, + bool isModerator, + DateTime createdAt, + DateTime updatedAt}) => + _Member( + userId: userId ?? this.userId, + channelCid: channelCid ?? this.channelCid, + role: role ?? this.role, + inviteAcceptedAt: inviteAcceptedAt ?? this.inviteAcceptedAt, + inviteRejectedAt: inviteRejectedAt ?? this.inviteRejectedAt, + invited: invited ?? this.invited, + banned: banned ?? this.banned, + shadowBanned: shadowBanned ?? this.shadowBanned, + isModerator: isModerator ?? this.isModerator, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + @override + String toString() { + return (StringBuffer('_Member(') + ..write('userId: $userId, ') + ..write('channelCid: $channelCid, ') + ..write('role: $role, ') + ..write('inviteAcceptedAt: $inviteAcceptedAt, ') + ..write('inviteRejectedAt: $inviteRejectedAt, ') + ..write('invited: $invited, ') + ..write('banned: $banned, ') + ..write('shadowBanned: $shadowBanned, ') + ..write('isModerator: $isModerator, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => $mrjf($mrjc( + userId.hashCode, + $mrjc( + channelCid.hashCode, + $mrjc( + role.hashCode, + $mrjc( + inviteAcceptedAt.hashCode, + $mrjc( + inviteRejectedAt.hashCode, + $mrjc( + invited.hashCode, + $mrjc( + banned.hashCode, + $mrjc( + shadowBanned.hashCode, + $mrjc( + isModerator.hashCode, + $mrjc(createdAt.hashCode, + updatedAt.hashCode))))))))))); + @override + bool operator ==(dynamic other) => + identical(this, other) || + (other is _Member && + other.userId == this.userId && + other.channelCid == this.channelCid && + other.role == this.role && + other.inviteAcceptedAt == this.inviteAcceptedAt && + other.inviteRejectedAt == this.inviteRejectedAt && + other.invited == this.invited && + other.banned == this.banned && + other.shadowBanned == this.shadowBanned && + other.isModerator == this.isModerator && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} + +class _MembersCompanion extends UpdateCompanion<_Member> { + final Value userId; + final Value channelCid; + final Value role; + final Value inviteAcceptedAt; + final Value inviteRejectedAt; + final Value invited; + final Value banned; + final Value shadowBanned; + final Value isModerator; + final Value createdAt; + final Value updatedAt; + const _MembersCompanion({ + this.userId = const Value.absent(), + this.channelCid = const Value.absent(), + this.role = const Value.absent(), + this.inviteAcceptedAt = const Value.absent(), + this.inviteRejectedAt = const Value.absent(), + this.invited = const Value.absent(), + this.banned = const Value.absent(), + this.shadowBanned = const Value.absent(), + this.isModerator = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + }); + _MembersCompanion.insert({ + @required String userId, + @required String channelCid, + this.role = const Value.absent(), + this.inviteAcceptedAt = const Value.absent(), + this.inviteRejectedAt = const Value.absent(), + this.invited = const Value.absent(), + this.banned = const Value.absent(), + this.shadowBanned = const Value.absent(), + this.isModerator = const Value.absent(), + @required DateTime createdAt, + this.updatedAt = const Value.absent(), + }) : userId = Value(userId), + channelCid = Value(channelCid), + createdAt = Value(createdAt); + static Insertable<_Member> custom({ + Expression userId, + Expression channelCid, + Expression role, + Expression inviteAcceptedAt, + Expression inviteRejectedAt, + Expression invited, + Expression banned, + Expression shadowBanned, + Expression isModerator, + Expression createdAt, + Expression updatedAt, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (channelCid != null) 'channel_cid': channelCid, + if (role != null) 'role': role, + if (inviteAcceptedAt != null) 'invite_accepted_at': inviteAcceptedAt, + if (inviteRejectedAt != null) 'invite_rejected_at': inviteRejectedAt, + if (invited != null) 'invited': invited, + if (banned != null) 'banned': banned, + if (shadowBanned != null) 'shadow_banned': shadowBanned, + if (isModerator != null) 'is_moderator': isModerator, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + }); + } + + _MembersCompanion copyWith( + {Value userId, + Value channelCid, + Value role, + Value inviteAcceptedAt, + Value inviteRejectedAt, + Value invited, + Value banned, + Value shadowBanned, + Value isModerator, + Value createdAt, + Value updatedAt}) { + return _MembersCompanion( + userId: userId ?? this.userId, + channelCid: channelCid ?? this.channelCid, + role: role ?? this.role, + inviteAcceptedAt: inviteAcceptedAt ?? this.inviteAcceptedAt, + inviteRejectedAt: inviteRejectedAt ?? this.inviteRejectedAt, + invited: invited ?? this.invited, + banned: banned ?? this.banned, + shadowBanned: shadowBanned ?? this.shadowBanned, + isModerator: isModerator ?? this.isModerator, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (channelCid.present) { + map['channel_cid'] = Variable(channelCid.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + if (inviteAcceptedAt.present) { + map['invite_accepted_at'] = Variable(inviteAcceptedAt.value); + } + if (inviteRejectedAt.present) { + map['invite_rejected_at'] = Variable(inviteRejectedAt.value); + } + if (invited.present) { + map['invited'] = Variable(invited.value); + } + if (banned.present) { + map['banned'] = Variable(banned.value); + } + if (shadowBanned.present) { + map['shadow_banned'] = Variable(shadowBanned.value); + } + if (isModerator.present) { + map['is_moderator'] = Variable(isModerator.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('_MembersCompanion(') + ..write('userId: $userId, ') + ..write('channelCid: $channelCid, ') + ..write('role: $role, ') + ..write('inviteAcceptedAt: $inviteAcceptedAt, ') + ..write('inviteRejectedAt: $inviteRejectedAt, ') + ..write('invited: $invited, ') + ..write('banned: $banned, ') + ..write('shadowBanned: $shadowBanned, ') + ..write('isModerator: $isModerator, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } +} + +class $_MembersTable extends _Members with TableInfo<$_MembersTable, _Member> { + final GeneratedDatabase _db; + final String _alias; + $_MembersTable(this._db, [this._alias]); + final VerificationMeta _userIdMeta = const VerificationMeta('userId'); + GeneratedTextColumn _userId; + @override + GeneratedTextColumn get userId => _userId ??= _constructUserId(); + GeneratedTextColumn _constructUserId() { + return GeneratedTextColumn( + 'user_id', + $tableName, + false, + ); + } + + final VerificationMeta _channelCidMeta = const VerificationMeta('channelCid'); + GeneratedTextColumn _channelCid; + @override + GeneratedTextColumn get channelCid => _channelCid ??= _constructChannelCid(); + GeneratedTextColumn _constructChannelCid() { + return GeneratedTextColumn( + 'channel_cid', + $tableName, + false, + ); + } + + final VerificationMeta _roleMeta = const VerificationMeta('role'); + GeneratedTextColumn _role; + @override + GeneratedTextColumn get role => _role ??= _constructRole(); + GeneratedTextColumn _constructRole() { + return GeneratedTextColumn( + 'role', + $tableName, + true, + ); + } + + final VerificationMeta _inviteAcceptedAtMeta = + const VerificationMeta('inviteAcceptedAt'); + GeneratedDateTimeColumn _inviteAcceptedAt; + @override + GeneratedDateTimeColumn get inviteAcceptedAt => + _inviteAcceptedAt ??= _constructInviteAcceptedAt(); + GeneratedDateTimeColumn _constructInviteAcceptedAt() { + return GeneratedDateTimeColumn( + 'invite_accepted_at', + $tableName, + true, + ); + } + + final VerificationMeta _inviteRejectedAtMeta = + const VerificationMeta('inviteRejectedAt'); + GeneratedDateTimeColumn _inviteRejectedAt; + @override + GeneratedDateTimeColumn get inviteRejectedAt => + _inviteRejectedAt ??= _constructInviteRejectedAt(); + GeneratedDateTimeColumn _constructInviteRejectedAt() { + return GeneratedDateTimeColumn( + 'invite_rejected_at', + $tableName, + true, + ); + } + + final VerificationMeta _invitedMeta = const VerificationMeta('invited'); + GeneratedBoolColumn _invited; + @override + GeneratedBoolColumn get invited => _invited ??= _constructInvited(); + GeneratedBoolColumn _constructInvited() { + return GeneratedBoolColumn( + 'invited', + $tableName, + true, + ); + } + + final VerificationMeta _bannedMeta = const VerificationMeta('banned'); + GeneratedBoolColumn _banned; + @override + GeneratedBoolColumn get banned => _banned ??= _constructBanned(); + GeneratedBoolColumn _constructBanned() { + return GeneratedBoolColumn( + 'banned', + $tableName, + true, + ); + } + + final VerificationMeta _shadowBannedMeta = + const VerificationMeta('shadowBanned'); + GeneratedBoolColumn _shadowBanned; + @override + GeneratedBoolColumn get shadowBanned => + _shadowBanned ??= _constructShadowBanned(); + GeneratedBoolColumn _constructShadowBanned() { + return GeneratedBoolColumn( + 'shadow_banned', + $tableName, + true, + ); + } + + final VerificationMeta _isModeratorMeta = + const VerificationMeta('isModerator'); + GeneratedBoolColumn _isModerator; + @override + GeneratedBoolColumn get isModerator => + _isModerator ??= _constructIsModerator(); + GeneratedBoolColumn _constructIsModerator() { + return GeneratedBoolColumn( + 'is_moderator', + $tableName, + true, + ); + } + + final VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); + GeneratedDateTimeColumn _createdAt; + @override + GeneratedDateTimeColumn get createdAt => _createdAt ??= _constructCreatedAt(); + GeneratedDateTimeColumn _constructCreatedAt() { + return GeneratedDateTimeColumn( + 'created_at', + $tableName, + false, + ); + } + + final VerificationMeta _updatedAtMeta = const VerificationMeta('updatedAt'); + GeneratedDateTimeColumn _updatedAt; + @override + GeneratedDateTimeColumn get updatedAt => _updatedAt ??= _constructUpdatedAt(); + GeneratedDateTimeColumn _constructUpdatedAt() { + return GeneratedDateTimeColumn( + 'updated_at', + $tableName, + true, + ); + } + + @override + List get $columns => [ + userId, + channelCid, + role, + inviteAcceptedAt, + inviteRejectedAt, + invited, + banned, + shadowBanned, + isModerator, + createdAt, + updatedAt + ]; + @override + $_MembersTable get asDslTable => this; + @override + String get $tableName => _alias ?? 'members'; + @override + final String actualTableName = 'members'; + @override + VerificationContext validateIntegrity(Insertable<_Member> instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id'], _userIdMeta)); + } else if (isInserting) { + context.missing(_userIdMeta); + } + if (data.containsKey('channel_cid')) { + context.handle( + _channelCidMeta, + channelCid.isAcceptableOrUnknown( + data['channel_cid'], _channelCidMeta)); + } else if (isInserting) { + context.missing(_channelCidMeta); + } + if (data.containsKey('role')) { + context.handle( + _roleMeta, role.isAcceptableOrUnknown(data['role'], _roleMeta)); + } + if (data.containsKey('invite_accepted_at')) { + context.handle( + _inviteAcceptedAtMeta, + inviteAcceptedAt.isAcceptableOrUnknown( + data['invite_accepted_at'], _inviteAcceptedAtMeta)); + } + if (data.containsKey('invite_rejected_at')) { + context.handle( + _inviteRejectedAtMeta, + inviteRejectedAt.isAcceptableOrUnknown( + data['invite_rejected_at'], _inviteRejectedAtMeta)); + } + if (data.containsKey('invited')) { + context.handle(_invitedMeta, + invited.isAcceptableOrUnknown(data['invited'], _invitedMeta)); + } + if (data.containsKey('banned')) { + context.handle(_bannedMeta, + banned.isAcceptableOrUnknown(data['banned'], _bannedMeta)); + } + if (data.containsKey('shadow_banned')) { + context.handle( + _shadowBannedMeta, + shadowBanned.isAcceptableOrUnknown( + data['shadow_banned'], _shadowBannedMeta)); + } + if (data.containsKey('is_moderator')) { + context.handle( + _isModeratorMeta, + isModerator.isAcceptableOrUnknown( + data['is_moderator'], _isModeratorMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at'], _createdAtMeta)); + } else if (isInserting) { + context.missing(_createdAtMeta); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at'], _updatedAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {userId, channelCid}; + @override + _Member map(Map data, {String tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; + return _Member.fromData(data, _db, prefix: effectivePrefix); + } + + @override + $_MembersTable createAlias(String alias) { + return $_MembersTable(_db, alias); + } +} + +class ChannelQuery extends DataClass implements Insertable { + final String queryHash; + final String channelCid; + ChannelQuery({@required this.queryHash, @required this.channelCid}); + factory ChannelQuery.fromData(Map data, GeneratedDatabase db, + {String prefix}) { + final effectivePrefix = prefix ?? ''; + final stringType = db.typeSystem.forDartType(); + return ChannelQuery( + queryHash: stringType + .mapFromDatabaseResponse(data['${effectivePrefix}query_hash']), + channelCid: stringType + .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid']), + ); + } + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (!nullToAbsent || queryHash != null) { + map['query_hash'] = Variable(queryHash); + } + if (!nullToAbsent || channelCid != null) { + map['channel_cid'] = Variable(channelCid); + } + return map; + } + + _ChannelQueriesCompanion toCompanion(bool nullToAbsent) { + return _ChannelQueriesCompanion( + queryHash: queryHash == null && nullToAbsent + ? const Value.absent() + : Value(queryHash), + channelCid: channelCid == null && nullToAbsent + ? const Value.absent() + : Value(channelCid), + ); + } + + factory ChannelQuery.fromJson(Map json, + {ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return ChannelQuery( + queryHash: serializer.fromJson(json['queryHash']), + channelCid: serializer.fromJson(json['channelCid']), + ); + } + @override + Map toJson({ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return { + 'queryHash': serializer.toJson(queryHash), + 'channelCid': serializer.toJson(channelCid), + }; + } + + ChannelQuery copyWith({String queryHash, String channelCid}) => ChannelQuery( + queryHash: queryHash ?? this.queryHash, + channelCid: channelCid ?? this.channelCid, + ); + @override + String toString() { + return (StringBuffer('ChannelQuery(') + ..write('queryHash: $queryHash, ') + ..write('channelCid: $channelCid') + ..write(')')) + .toString(); + } + + @override + int get hashCode => $mrjf($mrjc(queryHash.hashCode, channelCid.hashCode)); + @override + bool operator ==(dynamic other) => + identical(this, other) || + (other is ChannelQuery && + other.queryHash == this.queryHash && + other.channelCid == this.channelCid); +} + +class _ChannelQueriesCompanion extends UpdateCompanion { + final Value queryHash; + final Value channelCid; + const _ChannelQueriesCompanion({ + this.queryHash = const Value.absent(), + this.channelCid = const Value.absent(), + }); + _ChannelQueriesCompanion.insert({ + @required String queryHash, + @required String channelCid, + }) : queryHash = Value(queryHash), + channelCid = Value(channelCid); + static Insertable custom({ + Expression queryHash, + Expression channelCid, + }) { + return RawValuesInsertable({ + if (queryHash != null) 'query_hash': queryHash, + if (channelCid != null) 'channel_cid': channelCid, + }); + } + + _ChannelQueriesCompanion copyWith( + {Value queryHash, Value channelCid}) { + return _ChannelQueriesCompanion( + queryHash: queryHash ?? this.queryHash, + channelCid: channelCid ?? this.channelCid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (queryHash.present) { + map['query_hash'] = Variable(queryHash.value); + } + if (channelCid.present) { + map['channel_cid'] = Variable(channelCid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('_ChannelQueriesCompanion(') + ..write('queryHash: $queryHash, ') + ..write('channelCid: $channelCid') + ..write(')')) + .toString(); + } +} + +class $_ChannelQueriesTable extends _ChannelQueries + with TableInfo<$_ChannelQueriesTable, ChannelQuery> { + final GeneratedDatabase _db; + final String _alias; + $_ChannelQueriesTable(this._db, [this._alias]); + final VerificationMeta _queryHashMeta = const VerificationMeta('queryHash'); + GeneratedTextColumn _queryHash; + @override + GeneratedTextColumn get queryHash => _queryHash ??= _constructQueryHash(); + GeneratedTextColumn _constructQueryHash() { + return GeneratedTextColumn( + 'query_hash', + $tableName, + false, + ); + } + + final VerificationMeta _channelCidMeta = const VerificationMeta('channelCid'); + GeneratedTextColumn _channelCid; + @override + GeneratedTextColumn get channelCid => _channelCid ??= _constructChannelCid(); + GeneratedTextColumn _constructChannelCid() { + return GeneratedTextColumn( + 'channel_cid', + $tableName, + false, + ); + } + + @override + List get $columns => [queryHash, channelCid]; + @override + $_ChannelQueriesTable get asDslTable => this; + @override + String get $tableName => _alias ?? 'channel_queries'; + @override + final String actualTableName = 'channel_queries'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('query_hash')) { + context.handle(_queryHashMeta, + queryHash.isAcceptableOrUnknown(data['query_hash'], _queryHashMeta)); + } else if (isInserting) { + context.missing(_queryHashMeta); + } + if (data.containsKey('channel_cid')) { + context.handle( + _channelCidMeta, + channelCid.isAcceptableOrUnknown( + data['channel_cid'], _channelCidMeta)); + } else if (isInserting) { + context.missing(_channelCidMeta); + } + return context; + } + + @override + Set get $primaryKey => {queryHash, channelCid}; + @override + ChannelQuery map(Map data, {String tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; + return ChannelQuery.fromData(data, _db, prefix: effectivePrefix); + } + + @override + $_ChannelQueriesTable createAlias(String alias) { + return $_ChannelQueriesTable(_db, alias); + } +} + +class _Reaction extends DataClass implements Insertable<_Reaction> { + final String messageId; + final String type; + final DateTime createdAt; + final int score; + final String userId; + final Map extraData; + _Reaction( + {@required this.messageId, + @required this.type, + @required this.createdAt, + this.score, + @required this.userId, + this.extraData}); + factory _Reaction.fromData(Map data, GeneratedDatabase db, + {String prefix}) { + final effectivePrefix = prefix ?? ''; + final stringType = db.typeSystem.forDartType(); + final dateTimeType = db.typeSystem.forDartType(); + final intType = db.typeSystem.forDartType(); + return _Reaction( + messageId: stringType + .mapFromDatabaseResponse(data['${effectivePrefix}message_id']), + type: stringType.mapFromDatabaseResponse(data['${effectivePrefix}type']), + createdAt: dateTimeType + .mapFromDatabaseResponse(data['${effectivePrefix}created_at']), + score: intType.mapFromDatabaseResponse(data['${effectivePrefix}score']), + userId: + stringType.mapFromDatabaseResponse(data['${effectivePrefix}user_id']), + extraData: $_ReactionsTable.$converter0.mapToDart(stringType + .mapFromDatabaseResponse(data['${effectivePrefix}extra_data'])), + ); + } + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (!nullToAbsent || messageId != null) { + map['message_id'] = Variable(messageId); + } + if (!nullToAbsent || type != null) { + map['type'] = Variable(type); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || score != null) { + map['score'] = Variable(score); + } + if (!nullToAbsent || userId != null) { + map['user_id'] = Variable(userId); + } + if (!nullToAbsent || extraData != null) { + final converter = $_ReactionsTable.$converter0; + map['extra_data'] = Variable(converter.mapToSql(extraData)); + } + return map; + } + + _ReactionsCompanion toCompanion(bool nullToAbsent) { + return _ReactionsCompanion( + messageId: messageId == null && nullToAbsent + ? const Value.absent() + : Value(messageId), + type: type == null && nullToAbsent ? const Value.absent() : Value(type), + createdAt: createdAt == null && nullToAbsent + ? const Value.absent() + : Value(createdAt), + score: + score == null && nullToAbsent ? const Value.absent() : Value(score), + userId: + userId == null && nullToAbsent ? const Value.absent() : Value(userId), + extraData: extraData == null && nullToAbsent + ? const Value.absent() + : Value(extraData), + ); + } + + factory _Reaction.fromJson(Map json, + {ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return _Reaction( + messageId: serializer.fromJson(json['messageId']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + score: serializer.fromJson(json['score']), + userId: serializer.fromJson(json['userId']), + extraData: serializer.fromJson>(json['extraData']), + ); + } + @override + Map toJson({ValueSerializer serializer}) { + serializer ??= moorRuntimeOptions.defaultSerializer; + return { + 'messageId': serializer.toJson(messageId), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'score': serializer.toJson(score), + 'userId': serializer.toJson(userId), + 'extraData': serializer.toJson>(extraData), + }; + } + + _Reaction copyWith( + {String messageId, + String type, + DateTime createdAt, + int score, + String userId, + Map extraData}) => + _Reaction( + messageId: messageId ?? this.messageId, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + score: score ?? this.score, + userId: userId ?? this.userId, + extraData: extraData ?? this.extraData, + ); + @override + String toString() { + return (StringBuffer('_Reaction(') + ..write('messageId: $messageId, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('score: $score, ') + ..write('userId: $userId, ') + ..write('extraData: $extraData') + ..write(')')) + .toString(); + } + + @override + int get hashCode => $mrjf($mrjc( + messageId.hashCode, + $mrjc( + type.hashCode, + $mrjc( + createdAt.hashCode, + $mrjc(score.hashCode, + $mrjc(userId.hashCode, extraData.hashCode)))))); + @override + bool operator ==(dynamic other) => + identical(this, other) || + (other is _Reaction && + other.messageId == this.messageId && + other.type == this.type && + other.createdAt == this.createdAt && + other.score == this.score && + other.userId == this.userId && + other.extraData == this.extraData); +} + +class _ReactionsCompanion extends UpdateCompanion<_Reaction> { + final Value messageId; + final Value type; + final Value createdAt; + final Value score; + final Value userId; + final Value> extraData; + const _ReactionsCompanion({ + this.messageId = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.score = const Value.absent(), + this.userId = const Value.absent(), + this.extraData = const Value.absent(), + }); + _ReactionsCompanion.insert({ + @required String messageId, + @required String type, + @required DateTime createdAt, + this.score = const Value.absent(), + @required String userId, + this.extraData = const Value.absent(), + }) : messageId = Value(messageId), + type = Value(type), + createdAt = Value(createdAt), + userId = Value(userId); + static Insertable<_Reaction> custom({ + Expression messageId, + Expression type, + Expression createdAt, + Expression score, + Expression userId, + Expression extraData, + }) { + return RawValuesInsertable({ + if (messageId != null) 'message_id': messageId, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (score != null) 'score': score, + if (userId != null) 'user_id': userId, + if (extraData != null) 'extra_data': extraData, + }); + } + + _ReactionsCompanion copyWith( + {Value messageId, + Value type, + Value createdAt, + Value score, + Value userId, + Value> extraData}) { + return _ReactionsCompanion( + messageId: messageId ?? this.messageId, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + score: score ?? this.score, + userId: userId ?? this.userId, + extraData: extraData ?? this.extraData, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (score.present) { + map['score'] = Variable(score.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (extraData.present) { + final converter = $_ReactionsTable.$converter0; + map['extra_data'] = Variable(converter.mapToSql(extraData.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('_ReactionsCompanion(') + ..write('messageId: $messageId, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('score: $score, ') + ..write('userId: $userId, ') + ..write('extraData: $extraData') + ..write(')')) + .toString(); + } +} + +class $_ReactionsTable extends _Reactions + with TableInfo<$_ReactionsTable, _Reaction> { + final GeneratedDatabase _db; + final String _alias; + $_ReactionsTable(this._db, [this._alias]); + final VerificationMeta _messageIdMeta = const VerificationMeta('messageId'); + GeneratedTextColumn _messageId; + @override + GeneratedTextColumn get messageId => _messageId ??= _constructMessageId(); + GeneratedTextColumn _constructMessageId() { + return GeneratedTextColumn( + 'message_id', + $tableName, + false, + ); + } + + final VerificationMeta _typeMeta = const VerificationMeta('type'); + GeneratedTextColumn _type; + @override + GeneratedTextColumn get type => _type ??= _constructType(); + GeneratedTextColumn _constructType() { + return GeneratedTextColumn( + 'type', + $tableName, + false, + ); + } + + final VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); + GeneratedDateTimeColumn _createdAt; + @override + GeneratedDateTimeColumn get createdAt => _createdAt ??= _constructCreatedAt(); + GeneratedDateTimeColumn _constructCreatedAt() { + return GeneratedDateTimeColumn( + 'created_at', + $tableName, + false, + ); + } + + final VerificationMeta _scoreMeta = const VerificationMeta('score'); + GeneratedIntColumn _score; + @override + GeneratedIntColumn get score => _score ??= _constructScore(); + GeneratedIntColumn _constructScore() { + return GeneratedIntColumn( + 'score', + $tableName, + true, + ); + } + + final VerificationMeta _userIdMeta = const VerificationMeta('userId'); + GeneratedTextColumn _userId; + @override + GeneratedTextColumn get userId => _userId ??= _constructUserId(); + GeneratedTextColumn _constructUserId() { + return GeneratedTextColumn( + 'user_id', + $tableName, + false, + ); + } + + final VerificationMeta _extraDataMeta = const VerificationMeta('extraData'); + GeneratedTextColumn _extraData; + @override + GeneratedTextColumn get extraData => _extraData ??= _constructExtraData(); + GeneratedTextColumn _constructExtraData() { + return GeneratedTextColumn( + 'extra_data', + $tableName, + true, + ); + } + + @override + List get $columns => + [messageId, type, createdAt, score, userId, extraData]; + @override + $_ReactionsTable get asDslTable => this; + @override + String get $tableName => _alias ?? 'reactions'; + @override + final String actualTableName = 'reactions'; + @override + VerificationContext validateIntegrity(Insertable<_Reaction> instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('message_id')) { + context.handle(_messageIdMeta, + messageId.isAcceptableOrUnknown(data['message_id'], _messageIdMeta)); + } else if (isInserting) { + context.missing(_messageIdMeta); + } + if (data.containsKey('type')) { + context.handle( + _typeMeta, type.isAcceptableOrUnknown(data['type'], _typeMeta)); + } else if (isInserting) { + context.missing(_typeMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at'], _createdAtMeta)); + } else if (isInserting) { + context.missing(_createdAtMeta); + } + if (data.containsKey('score')) { + context.handle( + _scoreMeta, score.isAcceptableOrUnknown(data['score'], _scoreMeta)); + } + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id'], _userIdMeta)); + } else if (isInserting) { + context.missing(_userIdMeta); + } + context.handle(_extraDataMeta, const VerificationResult.success()); + return context; + } + + @override + Set get $primaryKey => {messageId, type, userId}; + @override + _Reaction map(Map data, {String tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; + return _Reaction.fromData(data, _db, prefix: effectivePrefix); + } + + @override + $_ReactionsTable createAlias(String alias) { + return $_ReactionsTable(_db, alias); + } + + static TypeConverter, String> $converter0 = + _ExtraDataConverter(); +} + +abstract class _$OfflineStorage extends GeneratedDatabase { + _$OfflineStorage(QueryExecutor e) : super(SqlTypeSystem.defaultInstance, e); + _$OfflineStorage.connect(DatabaseConnection c) : super.connect(c); + $_ConnectionEventTable _connectionEvent; + $_ConnectionEventTable get connectionEvent => + _connectionEvent ??= $_ConnectionEventTable(this); + $_ChannelsTable _channels; + $_ChannelsTable get channels => _channels ??= $_ChannelsTable(this); + $_UsersTable _users; + $_UsersTable get users => _users ??= $_UsersTable(this); + $_MessagesTable _messages; + $_MessagesTable get messages => _messages ??= $_MessagesTable(this); + $_ReadsTable _reads; + $_ReadsTable get reads => _reads ??= $_ReadsTable(this); + $_MembersTable _members; + $_MembersTable get members => _members ??= $_MembersTable(this); + $_ChannelQueriesTable _channelQueries; + $_ChannelQueriesTable get channelQueries => + _channelQueries ??= $_ChannelQueriesTable(this); + $_ReactionsTable _reactions; + $_ReactionsTable get reactions => _reactions ??= $_ReactionsTable(this); + @override + Iterable get allTables => allSchemaEntities.whereType(); + @override + List get allSchemaEntities => [ + connectionEvent, + channels, + users, + messages, + reads, + members, + channelQueries, + reactions + ]; +} diff --git a/packages/dart_client/lib/src/db/shared/native_db.dart b/packages/dart_client/lib/src/db/shared/native_db.dart new file mode 100644 index 00000000..38a1923e --- /dev/null +++ b/packages/dart_client/lib/src/db/shared/native_db.dart @@ -0,0 +1,64 @@ +import 'dart:io'; +import 'dart:isolate'; +import 'package:flutter/material.dart'; +import 'package:moor/ffi.dart'; +import 'package:moor/isolate.dart'; +import 'package:moor/moor.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:stream_chat/src/db/offline_storage.dart'; + +class SharedDB { + static constructDatabase(dbName) async { + final dir = await getApplicationDocumentsDirectory(); + final path = join(dir.path, dbName); + final file = File(path); + return VmDatabase(file); + } + + static Future createMoorIsolate(String userId) async { + WidgetsFlutterBinding.ensureInitialized(); + final dir = await getApplicationDocumentsDirectory(); + final path = join(dir.path, 'db_$userId.sqlite'); + + final receivePort = ReceivePort(); + await Isolate.spawn( + startBackground, + _IsolateStartRequest(receivePort.sendPort, path), + ); + + return (await receivePort.first as MoorIsolate); + } + + static void startBackground(_IsolateStartRequest request) { + final executor = LazyDatabase(() async { + return VmDatabase(File(request.targetPath)); + }); + final moorIsolate = MoorIsolate.inCurrent( + () => DatabaseConnection.fromExecutor(executor), + ); + request.sendMoorIsolate.send(moorIsolate); + } + + static Future constructOfflineStorage({ + userId, + logger, + }) async { + logger.info('Connecting on background isolate'); + final isolate = await createMoorIsolate(userId); + final connection = await isolate.connect(); + return OfflineStorage.connect( + connection, + userId, + isolate, + logger, + ); + } +} + +class _IsolateStartRequest { + final SendPort sendMoorIsolate; + final String targetPath; + + _IsolateStartRequest(this.sendMoorIsolate, this.targetPath); +} diff --git a/packages/dart_client/lib/src/db/shared/shared_db.dart b/packages/dart_client/lib/src/db/shared/shared_db.dart new file mode 100644 index 00000000..25120fcf --- /dev/null +++ b/packages/dart_client/lib/src/db/shared/shared_db.dart @@ -0,0 +1,3 @@ +export 'unsupported_db.dart' + if (dart.library.io) 'native_db.dart' // implementation using dart:io + if (dart.library.html) 'web_db.dart'; diff --git a/packages/dart_client/lib/src/db/shared/unsupported_db.dart b/packages/dart_client/lib/src/db/shared/unsupported_db.dart new file mode 100644 index 00000000..9d4ef84f --- /dev/null +++ b/packages/dart_client/lib/src/db/shared/unsupported_db.dart @@ -0,0 +1,12 @@ +class SharedDB { + static constructDatabase(dbName) async { + print('Unsupported Platform'); + return null; + } + + static createMoorIsolate(userId) {} + + static startBackground(request) {} + + static constructOfflineStorage({userId, logger}) {} +} diff --git a/packages/dart_client/lib/src/db/shared/web_db.dart b/packages/dart_client/lib/src/db/shared/web_db.dart new file mode 100644 index 00000000..bd579280 --- /dev/null +++ b/packages/dart_client/lib/src/db/shared/web_db.dart @@ -0,0 +1,18 @@ +import 'package:moor/moor_web.dart'; +import 'package:stream_chat/src/db/offline_storage.dart'; + +class SharedDB { + static constructDatabase(dbName) async { + return WebDatabase(dbName); + } + + static Future constructOfflineStorage({ + userId, + logger, + }) async { + return OfflineStorage( + userId, + logger, + ); + } +} diff --git a/packages/dart_client/lib/src/event_type.dart b/packages/dart_client/lib/src/event_type.dart new file mode 100644 index 00000000..fd93af2a --- /dev/null +++ b/packages/dart_client/lib/src/event_type.dart @@ -0,0 +1,94 @@ +/// This class defines some basic event types +class EventType { + /// Indicates any type of events + static const String any = '*'; + + /// Event sent when a user starts typing a message + static const String typingStart = 'typing.start'; + + /// Event sent when a user stops typing a message + static const String typingStop = 'typing.stop'; + + /// Event sent when receiving a new message + static const String messageNew = 'message.new'; + + /// Event sent when receiving a new message + static const String notificationMessageNew = 'notification.message_new'; + + /// Event sent when the unread count changes + static const String notificationMarkRead = 'notification.mark_read'; + + /// Event sent when deleting a new message + static const String messageDeleted = 'message.deleted'; + + /// Event sent when receiving a new reaction + static const String reactionNew = 'reaction.new'; + + /// Event sent when deleting a reaction + static const String reactionDeleted = 'reaction.deleted'; + + /// Event sent when updating a reaction + static const String reactionUpdated = 'reaction.updated'; + + /// Event sent when updating a message + static const String messageUpdated = 'message.updated'; + + /// Event sent when reading a message + static const String messageRead = 'message.read'; + + /// Event sent when a channel is deleted + static const String channelDeleted = 'channel.deleted'; + + /// Event sent when a channel is deleted + static const String notificationChannelDeleted = + 'notification.channel_deleted'; + + /// Event sent when a channel is truncated + static const String channelTruncated = 'channel.truncated'; + + /// Event sent when a channel is truncated + static const String notificationChannelTruncated = + 'notification.channel_truncated'; + + /// Event sent when the user is added to a channel + static const String notificationAddedToChannel = + 'notification.added_to_channel'; + + /// Event sent when the user is removed to a channel + static const String notificationRemovedFromChannel = + 'notification.removed_from_channel'; + + /// Event sent when a channel is updated + static const String channelUpdated = 'channel.updated'; + + /// Event sent when a user is updated + static const String userUpdated = 'user.updated'; + + /// Event sent when a member is added to a channel + static const String memberAdded = 'member.added'; + + /// Event sent when a member is removed to a channel + static const String memberRemoved = 'member.removed'; + + /// Event sent when a channel is hidden + static const String channelHidden = 'channel.hidden'; + + /// Event sent when a channel is visible + static const String channelVisible = 'channel.visible'; + + /// Event sent when the connection status changes + static const String connectionChanged = 'connection.changed'; + + /// Event sent when the connection is recovered + static const String connectionRecovered = 'connection.recovered'; + + /// Event sent when the user is accepts an invite + static const String notificationInviteAccepted = + 'notification.invite_accepted'; + + /// Event sent when the user is invited + static const String notificationInvited = 'notification.invited'; + + /// Event sent when the user's mutes list is updated + static const String notificationMutesUpdated = 'notification.mutes_updated'; +} diff --git a/packages/dart_client/lib/src/exceptions.dart b/packages/dart_client/lib/src/exceptions.dart new file mode 100644 index 00000000..9ba27e79 --- /dev/null +++ b/packages/dart_client/lib/src/exceptions.dart @@ -0,0 +1,54 @@ +import 'dart:convert'; + +/// Exception related to api calls +class ApiError extends Error { + /// Raw body of the response + final String body; + + /// Json parsed body + final Map jsonData; + + /// Http status code of the response + final int status; + + /// Stream specific error code + int get code => _code; + int _code; + + static Map _decode(String body) { + try { + if (body == null) { + return null; + } + return json.decode(body); + } on FormatException { + return null; + } + } + + /// Creates a new ApiError instance using the response body and status code + ApiError(this.body, this.status) : jsonData = _decode(body) { + if (jsonData != null && jsonData.containsKey('code')) { + _code = jsonData['code']; + } + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ApiError && + runtimeType == other.runtimeType && + body == other.body && + jsonData == other.jsonData && + status == other.status && + _code == other._code; + + @override + int get hashCode => + body.hashCode ^ jsonData.hashCode ^ status.hashCode ^ _code.hashCode; + + @override + String toString() { + return 'ApiError{body: $body, jsonData: $jsonData, status: $status, code: $_code}'; + } +} diff --git a/packages/dart_client/lib/src/models/action.dart b/packages/dart_client/lib/src/models/action.dart new file mode 100644 index 00000000..34bb8f7e --- /dev/null +++ b/packages/dart_client/lib/src/models/action.dart @@ -0,0 +1,31 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'action.g.dart'; + +/// The class that contains the information about an action +@JsonSerializable() +class Action { + /// The name of the action + final String name; + + /// The style of the action + final String style; + + /// The test of the action + final String text; + + /// The type of the action + final String type; + + /// The value of the action + final String value; + + /// Constructor used for json serialization + Action({this.name, this.style, this.text, this.type, this.value}); + + /// Create a new instance from a json + factory Action.fromJson(Map json) => _$ActionFromJson(json); + + /// Serialize to json + Map toJson() => _$ActionToJson(this); +} diff --git a/packages/dart_client/lib/src/models/action.g.dart b/packages/dart_client/lib/src/models/action.g.dart new file mode 100644 index 00000000..3c567843 --- /dev/null +++ b/packages/dart_client/lib/src/models/action.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'action.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Action _$ActionFromJson(Map json) { + return Action( + name: json['name'] as String, + style: json['style'] as String, + text: json['text'] as String, + type: json['type'] as String, + value: json['value'] as String, + ); +} + +Map _$ActionToJson(Action instance) => { + 'name': instance.name, + 'style': instance.style, + 'text': instance.text, + 'type': instance.type, + 'value': instance.value, + }; diff --git a/packages/dart_client/lib/src/models/attachment.dart b/packages/dart_client/lib/src/models/attachment.dart new file mode 100644 index 00000000..8546fb9d --- /dev/null +++ b/packages/dart_client/lib/src/models/attachment.dart @@ -0,0 +1,207 @@ +// ignore_for_file: public_member_api_docs + +import 'package:json_annotation/json_annotation.dart'; + +import 'action.dart'; +import 'serialization.dart'; + +part 'attachment.g.dart'; + +/// The class that contains the information about an attachment +@JsonSerializable(includeIfNull: false) +class Attachment { + ///The attachment type based on the URL resource. This can be: audio, image or video + final String type; + + ///The link to which the attachment message points to. + final String titleLink; + + /// The attachment title + final String title; + + /// The URL to the attached file thumbnail. You can use this to represent the attached link. + final String thumbUrl; + + /// The attachment text. It will be displayed in the channel next to the original message. + final String text; + + /// Optional text that appears above the attachment block + final String pretext; + + /// The original URL that was used to scrape this attachment. + final String ogScrapeUrl; + + /// The URL to the attached image. This is present for URL pointing to an image article (eg. Unsplash) + final String imageUrl; + final String footerIcon; + final String footer; + final dynamic fields; + final String fallback; + final String color; + + /// The name of the author. + final String authorName; + final String authorLink; + final String authorIcon; + + /// The URL to the audio, video or image related to the URL. + final String assetUrl; + + /// Actions from a command + final List actions; + + final Uri localUri; + + /// Map of custom channel extraData + @JsonKey(includeIfNull: false) + final Map extraData; + + /// Known top level fields. + /// Useful for [Serialization] methods. + static const topLevelFields = [ + 'type', + 'title_link', + 'title', + 'thumb_url', + 'text', + 'pretext', + 'og_scrape_url', + 'image_url', + 'footer_icon', + 'footer', + 'fields', + 'fallback', + 'color', + 'author_name', + 'author_link', + 'author_icon', + 'asset_url', + 'actions', + ]; + + /// Constructor used for json serialization + Attachment({ + this.type, + this.titleLink, + this.title, + this.thumbUrl, + this.text, + this.pretext, + this.ogScrapeUrl, + this.imageUrl, + this.footerIcon, + this.footer, + this.fields, + this.fallback, + this.color, + this.authorName, + this.authorLink, + this.authorIcon, + this.assetUrl, + this.actions, + this.extraData, + this.localUri, + }); + + /// Create a new instance from a json + factory Attachment.fromJson(Map json) { + return _$AttachmentFromJson( + Serialization.moveToExtraDataFromRoot(json, topLevelFields)); + } + + /// Serialize to json + Map toJson() => Serialization.moveFromExtraDataToRoot( + _$AttachmentToJson(this), topLevelFields); + + Attachment copyWith({ + String type, + String titleLink, + String title, + String thumbUrl, + String text, + String pretext, + String ogScrapeUrl, + String imageUrl, + String footerIcon, + String footer, + dynamic fields, + String fallback, + String color, + String authorName, + String authorLink, + String authorIcon, + String assetUrl, + List actions, + Uri localUri, + Map extraData, + }) => + Attachment( + type: type ?? this.type, + titleLink: titleLink ?? this.titleLink, + title: title ?? this.title, + thumbUrl: thumbUrl ?? this.thumbUrl, + text: text ?? this.text, + pretext: pretext ?? this.pretext, + ogScrapeUrl: ogScrapeUrl ?? this.ogScrapeUrl, + imageUrl: imageUrl ?? this.imageUrl, + footerIcon: footerIcon ?? this.footerIcon, + footer: footer ?? this.footer, + fields: fields ?? this.fields, + fallback: fallback ?? this.fallback, + color: color ?? this.color, + authorName: authorName ?? this.authorName, + authorLink: authorLink ?? this.authorLink, + authorIcon: authorIcon ?? this.authorIcon, + assetUrl: assetUrl ?? this.assetUrl, + actions: actions ?? this.actions, + localUri: localUri ?? this.localUri, + extraData: extraData ?? this.extraData, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Attachment && + runtimeType == other.runtimeType && + type == other.type && + titleLink == other.titleLink && + title == other.title && + thumbUrl == other.thumbUrl && + text == other.text && + pretext == other.pretext && + ogScrapeUrl == other.ogScrapeUrl && + imageUrl == other.imageUrl && + footerIcon == other.footerIcon && + footer == other.footer && + fields == other.fields && + fallback == other.fallback && + color == other.color && + authorName == other.authorName && + authorLink == other.authorLink && + authorIcon == other.authorIcon && + assetUrl == other.assetUrl && + actions == other.actions && + extraData == other.extraData; + + @override + int get hashCode => + type.hashCode ^ + titleLink.hashCode ^ + title.hashCode ^ + thumbUrl.hashCode ^ + text.hashCode ^ + pretext.hashCode ^ + ogScrapeUrl.hashCode ^ + imageUrl.hashCode ^ + footerIcon.hashCode ^ + footer.hashCode ^ + fields.hashCode ^ + fallback.hashCode ^ + color.hashCode ^ + authorName.hashCode ^ + authorLink.hashCode ^ + authorIcon.hashCode ^ + assetUrl.hashCode ^ + actions.hashCode ^ + extraData.hashCode; +} diff --git a/packages/dart_client/lib/src/models/attachment.g.dart b/packages/dart_client/lib/src/models/attachment.g.dart new file mode 100644 index 00000000..db276d69 --- /dev/null +++ b/packages/dart_client/lib/src/models/attachment.g.dart @@ -0,0 +1,74 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'attachment.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Attachment _$AttachmentFromJson(Map json) { + return Attachment( + type: json['type'] as String, + titleLink: json['title_link'] as String, + title: json['title'] as String, + thumbUrl: json['thumb_url'] as String, + text: json['text'] as String, + pretext: json['pretext'] as String, + ogScrapeUrl: json['og_scrape_url'] as String, + imageUrl: json['image_url'] as String, + footerIcon: json['footer_icon'] as String, + footer: json['footer'] as String, + fields: json['fields'], + fallback: json['fallback'] as String, + color: json['color'] as String, + authorName: json['author_name'] as String, + authorLink: json['author_link'] as String, + authorIcon: json['author_icon'] as String, + assetUrl: json['asset_url'] as String, + actions: (json['actions'] as List) + ?.map((e) => e == null + ? null + : Action.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(), + extraData: (json['extra_data'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + ), + localUri: json['local_uri'] == null + ? null + : Uri.parse(json['local_uri'] as String), + ); +} + +Map _$AttachmentToJson(Attachment instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('type', instance.type); + writeNotNull('title_link', instance.titleLink); + writeNotNull('title', instance.title); + writeNotNull('thumb_url', instance.thumbUrl); + writeNotNull('text', instance.text); + writeNotNull('pretext', instance.pretext); + writeNotNull('og_scrape_url', instance.ogScrapeUrl); + writeNotNull('image_url', instance.imageUrl); + writeNotNull('footer_icon', instance.footerIcon); + writeNotNull('footer', instance.footer); + writeNotNull('fields', instance.fields); + writeNotNull('fallback', instance.fallback); + writeNotNull('color', instance.color); + writeNotNull('author_name', instance.authorName); + writeNotNull('author_link', instance.authorLink); + writeNotNull('author_icon', instance.authorIcon); + writeNotNull('asset_url', instance.assetUrl); + writeNotNull('actions', instance.actions?.map((e) => e?.toJson())?.toList()); + writeNotNull('local_uri', instance.localUri?.toString()); + writeNotNull('extra_data', instance.extraData); + return val; +} diff --git a/packages/dart_client/lib/src/models/channel_config.dart b/packages/dart_client/lib/src/models/channel_config.dart new file mode 100644 index 00000000..e455930f --- /dev/null +++ b/packages/dart_client/lib/src/models/channel_config.dart @@ -0,0 +1,84 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'command.dart'; + +part 'channel_config.g.dart'; + +/// The class that contains the information about the configuration of a channel +@JsonSerializable() +class ChannelConfig { + /// Moderation configuration + final String automod; + + /// List of available commands + final List commands; + + /// True if the channel should send connect events + final bool connectEvents; + + /// Date of channel creation + final DateTime createdAt; + + /// Date of last channel update + final DateTime updatedAt; + + /// Max channel message length + final int maxMessageLength; + + /// Duration of message retention + final String messageRetention; + + /// True if users can be muted + final bool mutes; + + /// Name of the channel + final String name; + + /// True if reaction are active for this channel + final bool reactions; + + /// True if readEvents are active for this channel + final bool readEvents; + + /// True if reply message are active for this channel + final bool replies; + + /// True if it's possible to perform a search in this channel + final bool search; + + /// True if typing events should be sent for this channel + final bool typingEvents; + + /// True if it's possible to upload files to this channel + final bool uploads; + + /// True if urls appears as attachments + final bool urlEnrichment; + + /// Constructor used for json serialization + ChannelConfig({ + this.automod, + this.commands, + this.connectEvents, + this.createdAt, + this.updatedAt, + this.maxMessageLength, + this.messageRetention, + this.mutes, + this.name, + this.reactions, + this.readEvents, + this.replies, + this.search, + this.typingEvents, + this.uploads, + this.urlEnrichment, + }); + + /// Create a new instance from a json + factory ChannelConfig.fromJson(Map json) => + _$ChannelConfigFromJson(json); + + /// Serialize to json + Map toJson() => _$ChannelConfigToJson(this); +} diff --git a/packages/dart_client/lib/src/models/channel_config.g.dart b/packages/dart_client/lib/src/models/channel_config.g.dart new file mode 100644 index 00000000..e8152c80 --- /dev/null +++ b/packages/dart_client/lib/src/models/channel_config.g.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'channel_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ChannelConfig _$ChannelConfigFromJson(Map json) { + return ChannelConfig( + automod: json['automod'] as String, + commands: (json['commands'] as List) + ?.map((e) => e == null + ? null + : Command.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(), + connectEvents: json['connect_events'] as bool, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + maxMessageLength: json['max_message_length'] as int, + messageRetention: json['message_retention'] as String, + mutes: json['mutes'] as bool, + name: json['name'] as String, + reactions: json['reactions'] as bool, + readEvents: json['read_events'] as bool, + replies: json['replies'] as bool, + search: json['search'] as bool, + typingEvents: json['typing_events'] as bool, + uploads: json['uploads'] as bool, + urlEnrichment: json['url_enrichment'] as bool, + ); +} + +Map _$ChannelConfigToJson(ChannelConfig instance) => + { + 'automod': instance.automod, + 'commands': instance.commands?.map((e) => e?.toJson())?.toList(), + 'connect_events': instance.connectEvents, + 'created_at': instance.createdAt?.toIso8601String(), + 'updated_at': instance.updatedAt?.toIso8601String(), + 'max_message_length': instance.maxMessageLength, + 'message_retention': instance.messageRetention, + 'mutes': instance.mutes, + 'name': instance.name, + 'reactions': instance.reactions, + 'read_events': instance.readEvents, + 'replies': instance.replies, + 'search': instance.search, + 'typing_events': instance.typingEvents, + 'uploads': instance.uploads, + 'url_enrichment': instance.urlEnrichment, + }; diff --git a/packages/dart_client/lib/src/models/channel_model.dart b/packages/dart_client/lib/src/models/channel_model.dart new file mode 100644 index 00000000..10f65e0b --- /dev/null +++ b/packages/dart_client/lib/src/models/channel_model.dart @@ -0,0 +1,145 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'channel_config.dart'; +import 'serialization.dart'; +import 'user.dart'; + +part 'channel_model.g.dart'; + +/// The class that contains the information about a channel +@JsonSerializable() +class ChannelModel { + /// The id of this channel + final String id; + + /// The type of this channel + final String type; + + /// The cid of this channel + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final String cid; + + /// The channel configuration data + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final ChannelConfig config; + + /// The user that created this channel + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final User createdBy; + + /// True if this channel is frozen + @JsonKey(includeIfNull: false) + final bool frozen; + + /// The date of the last message + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final DateTime lastMessageAt; + + /// The date of channel creation + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final DateTime createdAt; + + /// The date of the last channel update + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final DateTime updatedAt; + + /// The date of channel deletion + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final DateTime deletedAt; + + /// The count of this channel members + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final int memberCount; + + /// Map of custom channel extraData + @JsonKey(includeIfNull: false) + final Map extraData; + + /// The team the channel belongs to + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final String team; + + /// Known top level fields. + /// Useful for [Serialization] methods. + static const topLevelFields = [ + 'id', + 'type', + 'cid', + 'config', + 'created_by', + 'frozen', + 'last_message_at', + 'created_at', + 'updated_at', + 'deleted_at', + 'member_count', + 'team', + ]; + + /// Constructor used for json serialization + ChannelModel({ + this.id, + this.type, + this.cid, + this.config, + this.createdBy, + this.frozen, + this.lastMessageAt, + this.createdAt, + this.updatedAt, + this.deletedAt, + this.memberCount, + this.extraData, + this.team, + }); + + /// Shortcut for channel name + String get name => + extraData?.containsKey('name') == true ? extraData['name'] : cid; + + /// Create a new instance from a json + factory ChannelModel.fromJson(Map json) { + return _$ChannelModelFromJson( + Serialization.moveToExtraDataFromRoot(json, topLevelFields)); + } + + /// Serialize to json + Map toJson() { + return Serialization.moveFromExtraDataToRoot( + _$ChannelModelToJson(this), + topLevelFields, + ); + } + + /// Creates a copy of [ChannelModel] with specified attributes overridden. + ChannelModel copyWith({ + String id, + String type, + String cid, + ChannelConfig config, + User createdBy, + bool frozen, + DateTime lastMessageAt, + DateTime createdAt, + DateTime updatedAt, + DateTime deletedAt, + int memberCount, + Map extraData, + String team, + }) => + ChannelModel( + id: id ?? this.id, + type: type ?? this.type, + cid: cid ?? this.cid, + config: config ?? this.config, + createdBy: createdBy ?? this.createdBy, + frozen: frozen ?? this.frozen, + lastMessageAt: lastMessageAt ?? this.lastMessageAt, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + memberCount: memberCount ?? this.memberCount, + extraData: extraData ?? this.extraData, + team: team ?? this.team, + ); +} diff --git a/packages/dart_client/lib/src/models/channel_model.g.dart b/packages/dart_client/lib/src/models/channel_model.g.dart new file mode 100644 index 00000000..4f535f46 --- /dev/null +++ b/packages/dart_client/lib/src/models/channel_model.g.dart @@ -0,0 +1,69 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'channel_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ChannelModel _$ChannelModelFromJson(Map json) { + return ChannelModel( + id: json['id'] as String, + type: json['type'] as String, + cid: json['cid'] as String, + config: json['config'] == null + ? null + : ChannelConfig.fromJson((json['config'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + createdBy: json['created_by'] == null + ? null + : User.fromJson((json['created_by'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + frozen: json['frozen'] as bool, + lastMessageAt: json['last_message_at'] == null + ? null + : DateTime.parse(json['last_message_at'] as String), + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + deletedAt: json['deleted_at'] == null + ? null + : DateTime.parse(json['deleted_at'] as String), + memberCount: json['member_count'] as int, + extraData: (json['extra_data'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + ), + team: json['team'] as String, + ); +} + +Map _$ChannelModelToJson(ChannelModel instance) { + final val = { + 'id': instance.id, + 'type': instance.type, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('cid', readonly(instance.cid)); + writeNotNull('config', readonly(instance.config)); + writeNotNull('created_by', readonly(instance.createdBy)); + writeNotNull('frozen', instance.frozen); + writeNotNull('last_message_at', readonly(instance.lastMessageAt)); + writeNotNull('created_at', readonly(instance.createdAt)); + writeNotNull('updated_at', readonly(instance.updatedAt)); + writeNotNull('deleted_at', readonly(instance.deletedAt)); + writeNotNull('member_count', readonly(instance.memberCount)); + writeNotNull('extra_data', instance.extraData); + writeNotNull('team', readonly(instance.team)); + return val; +} diff --git a/packages/dart_client/lib/src/models/channel_state.dart b/packages/dart_client/lib/src/models/channel_state.dart new file mode 100644 index 00000000..59e7a4f1 --- /dev/null +++ b/packages/dart_client/lib/src/models/channel_state.dart @@ -0,0 +1,66 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../models/read.dart'; +import '../models/user.dart'; +import 'channel_model.dart'; +import 'member.dart'; +import 'message.dart'; + +part 'channel_state.g.dart'; + +/// The class that contains the information about a command +@JsonSerializable() +class ChannelState { + /// The channel to which this state belongs + final ChannelModel channel; + + /// A paginated list of channel messages + final List messages; + + /// A paginated list of channel members + final List members; + + /// The count of users watching the channel + final int watcherCount; + + /// A paginated list of users watching the channel + final List watchers; + + /// The list of channel reads + final List read; + + /// Constructor used for json serialization + ChannelState({ + this.channel, + this.messages = const [], + this.members = const [], + this.watcherCount, + this.watchers = const [], + this.read = const [], + }); + + /// Create a new instance from a json + static ChannelState fromJson(Map json) => + _$ChannelStateFromJson(json); + + /// Serialize to json + Map toJson() => _$ChannelStateToJson(this); + + /// Creates a copy of [ChannelState] with specified attributes overridden. + ChannelState copyWith({ + ChannelModel channel, + List messages, + List members, + int watcherCount, + List watchers, + List read, + }) => + ChannelState( + channel: channel ?? this.channel, + messages: messages ?? this.messages, + members: members ?? this.members, + watcherCount: watcherCount ?? this.watcherCount, + watchers: watchers ?? this.watchers, + read: read ?? this.read, + ); +} diff --git a/packages/dart_client/lib/src/models/channel_state.g.dart b/packages/dart_client/lib/src/models/channel_state.g.dart new file mode 100644 index 00000000..d053768e --- /dev/null +++ b/packages/dart_client/lib/src/models/channel_state.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'channel_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ChannelState _$ChannelStateFromJson(Map json) { + return ChannelState( + channel: json['channel'] == null + ? null + : ChannelModel.fromJson((json['channel'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + messages: (json['messages'] as List) + ?.map((e) => e == null + ? null + : Message.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(), + members: (json['members'] as List) + ?.map((e) => e == null + ? null + : Member.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(), + watcherCount: json['watcher_count'] as int, + watchers: (json['watchers'] as List) + ?.map((e) => e == null + ? null + : User.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(), + read: (json['read'] as List) + ?.map((e) => e == null + ? null + : Read.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(), + ); +} + +Map _$ChannelStateToJson(ChannelState instance) => + { + 'channel': instance.channel?.toJson(), + 'messages': instance.messages?.map((e) => e?.toJson())?.toList(), + 'members': instance.members?.map((e) => e?.toJson())?.toList(), + 'watcher_count': instance.watcherCount, + 'watchers': instance.watchers?.map((e) => e?.toJson())?.toList(), + 'read': instance.read?.map((e) => e?.toJson())?.toList(), + }; diff --git a/packages/dart_client/lib/src/models/command.dart b/packages/dart_client/lib/src/models/command.dart new file mode 100644 index 00000000..420f2195 --- /dev/null +++ b/packages/dart_client/lib/src/models/command.dart @@ -0,0 +1,30 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'command.g.dart'; + +/// The class that contains the information about a command +@JsonSerializable() +class Command { + /// The name of the command + final String name; + + /// The description explaining the command + final String description; + + /// The arguments of the command + final String args; + + /// Constructor used for json serialization + Command({ + this.name, + this.description, + this.args, + }); + + /// Create a new instance from a json + factory Command.fromJson(Map json) => + _$CommandFromJson(json); + + /// Serialize to json + Map toJson() => _$CommandToJson(this); +} diff --git a/packages/dart_client/lib/src/models/command.g.dart b/packages/dart_client/lib/src/models/command.g.dart new file mode 100644 index 00000000..f32e8e8a --- /dev/null +++ b/packages/dart_client/lib/src/models/command.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'command.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Command _$CommandFromJson(Map json) { + return Command( + name: json['name'] as String, + description: json['description'] as String, + args: json['args'] as String, + ); +} + +Map _$CommandToJson(Command instance) => { + 'name': instance.name, + 'description': instance.description, + 'args': instance.args, + }; diff --git a/packages/dart_client/lib/src/models/device.dart b/packages/dart_client/lib/src/models/device.dart new file mode 100644 index 00000000..97247222 --- /dev/null +++ b/packages/dart_client/lib/src/models/device.dart @@ -0,0 +1,25 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'device.g.dart'; + +/// The class that contains the information about a device +@JsonSerializable() +class Device { + /// The id of the device + final String id; + + /// The notification push provider + final String pushProvider; + + /// Constructor used for json serialization + Device({ + this.id, + this.pushProvider, + }); + + /// Create a new instance from a json + factory Device.fromJson(Map json) => _$DeviceFromJson(json); + + /// Serialize to json + Map toJson() => _$DeviceToJson(this); +} diff --git a/packages/dart_client/lib/src/models/device.g.dart b/packages/dart_client/lib/src/models/device.g.dart new file mode 100644 index 00000000..bac60856 --- /dev/null +++ b/packages/dart_client/lib/src/models/device.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'device.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Device _$DeviceFromJson(Map json) { + return Device( + id: json['id'] as String, + pushProvider: json['push_provider'] as String, + ); +} + +Map _$DeviceToJson(Device instance) => { + 'id': instance.id, + 'push_provider': instance.pushProvider, + }; diff --git a/packages/dart_client/lib/src/models/event.dart b/packages/dart_client/lib/src/models/event.dart new file mode 100644 index 00000000..20537185 --- /dev/null +++ b/packages/dart_client/lib/src/models/event.dart @@ -0,0 +1,183 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:stream_chat/src/models/channel_model.dart'; +import 'package:stream_chat/src/models/message.dart'; +import 'package:stream_chat/src/models/serialization.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import '../event_type.dart'; +import 'member.dart'; +import 'own_user.dart'; +import 'reaction.dart'; +import 'user.dart'; + +part 'event.g.dart'; + +/// The class that contains the information about an event +@JsonSerializable() +class Event { + /// The type of the event + /// [EventType] contains some predefined constant types + final String type; + + /// The channel cid to which the event belongs + final String cid; + + /// The channel id to which the event belongs + final String channelId; + + /// The channel type to which the event belongs + final String channelType; + + /// The connection id in which the event has been sent + final String connectionId; + + /// The date of creation of the event + final DateTime createdAt; + + /// User object of the health check user + final OwnUser me; + + /// User object of the current user + final User user; + + /// The message sent with the event + final Message message; + + /// The channel sent with the event + final EventChannel channel; + + /// The member sent with the event + final Member member; + + /// The reaction sent with the event + final Reaction reaction; + + /// The number of unread messages for current user + final int totalUnreadCount; + + /// User total unread channels + final int unreadChannels; + + /// Online status + final bool online; + + /// The id of the parent message of a thread + final String parentId; + + /// True if the event is generated by this client + bool isLocal; + + /// Map of custom channel extraData + @JsonKey(includeIfNull: false) + final Map extraData; + + /// Constructor used for json serialization + Event({ + this.type, + this.cid, + this.connectionId, + this.createdAt, + this.me, + this.user, + this.message, + this.totalUnreadCount, + this.unreadChannels, + this.reaction, + this.online, + this.channel, + this.member, + this.channelId, + this.channelType, + this.parentId, + this.extraData, + }) : isLocal = true; + + /// Known top level fields. + /// Useful for [Serialization] methods. + static final topLevelFields = [ + 'type', + 'cid', + 'connection_id', + 'created_at', + 'me', + 'user', + 'message', + 'total_unread_count', + 'unread_channels', + 'reaction', + 'online', + 'channel', + 'member', + 'channel_id', + 'channel_type', + 'parent_id', + 'is_local', + ]; + + /// Create a new instance from a json + factory Event.fromJson(Map json) { + return _$EventFromJson(Serialization.moveToExtraDataFromRoot( + json, + topLevelFields, + )) + ..isLocal = false; + } + + /// Serialize to json + Map toJson() => Serialization.moveFromExtraDataToRoot( + _$EventToJson(this), + topLevelFields, + ); +} + +/// The channel embedded in the event object +@JsonSerializable() +class EventChannel extends ChannelModel { + /// A paginated list of channel members + final List members; + + /// Known top level fields. + /// Useful for [Serialization] methods. + static final topLevelFields = [ + 'members', + ...ChannelModel.topLevelFields, + ]; + + /// Constructor used for json serialization + EventChannel({ + this.members, + String id, + String type, + String cid, + ChannelConfig config, + User createdBy, + bool frozen, + DateTime lastMessageAt, + DateTime createdAt, + DateTime updatedAt, + DateTime deletedAt, + int memberCount, + Map extraData, + }) : super( + id: id, + type: type, + cid: cid, + config: config, + createdBy: createdBy, + frozen: frozen, + lastMessageAt: lastMessageAt, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + memberCount: memberCount, + extraData: extraData, + ); + + /// Create a new instance from a json + factory EventChannel.fromJson(Map json) { + return _$EventChannelFromJson(Serialization.moveToExtraDataFromRoot( + json, + topLevelFields, + )); + } +} diff --git a/packages/dart_client/lib/src/models/event.g.dart b/packages/dart_client/lib/src/models/event.g.dart new file mode 100644 index 00000000..aaef8181 --- /dev/null +++ b/packages/dart_client/lib/src/models/event.g.dart @@ -0,0 +1,156 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'event.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Event _$EventFromJson(Map json) { + return Event( + type: json['type'] as String, + cid: json['cid'] as String, + connectionId: json['connection_id'] as String, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + me: json['me'] == null + ? null + : OwnUser.fromJson((json['me'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + user: json['user'] == null + ? null + : User.fromJson((json['user'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + message: json['message'] == null + ? null + : Message.fromJson((json['message'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + totalUnreadCount: json['total_unread_count'] as int, + unreadChannels: json['unread_channels'] as int, + reaction: json['reaction'] == null + ? null + : Reaction.fromJson((json['reaction'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + online: json['online'] as bool, + channel: json['channel'] == null + ? null + : EventChannel.fromJson((json['channel'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + member: json['member'] == null + ? null + : Member.fromJson((json['member'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + channelId: json['channel_id'] as String, + channelType: json['channel_type'] as String, + parentId: json['parent_id'] as String, + extraData: (json['extra_data'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + ), + )..isLocal = json['is_local'] as bool; +} + +Map _$EventToJson(Event instance) { + final val = { + 'type': instance.type, + 'cid': instance.cid, + 'channel_id': instance.channelId, + 'channel_type': instance.channelType, + 'connection_id': instance.connectionId, + 'created_at': instance.createdAt?.toIso8601String(), + 'me': instance.me?.toJson(), + 'user': instance.user?.toJson(), + 'message': instance.message?.toJson(), + 'channel': instance.channel?.toJson(), + 'member': instance.member?.toJson(), + 'reaction': instance.reaction?.toJson(), + 'total_unread_count': instance.totalUnreadCount, + 'unread_channels': instance.unreadChannels, + 'online': instance.online, + 'parent_id': instance.parentId, + 'is_local': instance.isLocal, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('extra_data', instance.extraData); + return val; +} + +EventChannel _$EventChannelFromJson(Map json) { + return EventChannel( + members: (json['members'] as List) + ?.map((e) => e == null + ? null + : Member.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(), + id: json['id'] as String, + type: json['type'] as String, + cid: json['cid'] as String, + config: json['config'] == null + ? null + : ChannelConfig.fromJson((json['config'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + createdBy: json['created_by'] == null + ? null + : User.fromJson((json['created_by'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + frozen: json['frozen'] as bool, + lastMessageAt: json['last_message_at'] == null + ? null + : DateTime.parse(json['last_message_at'] as String), + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + deletedAt: json['deleted_at'] == null + ? null + : DateTime.parse(json['deleted_at'] as String), + memberCount: json['member_count'] as int, + extraData: (json['extra_data'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + ), + ); +} + +Map _$EventChannelToJson(EventChannel instance) { + final val = { + 'id': instance.id, + 'type': instance.type, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('cid', readonly(instance.cid)); + writeNotNull('config', readonly(instance.config)); + writeNotNull('created_by', readonly(instance.createdBy)); + writeNotNull('frozen', instance.frozen); + writeNotNull('last_message_at', readonly(instance.lastMessageAt)); + writeNotNull('created_at', readonly(instance.createdAt)); + writeNotNull('updated_at', readonly(instance.updatedAt)); + writeNotNull('deleted_at', readonly(instance.deletedAt)); + writeNotNull('member_count', readonly(instance.memberCount)); + writeNotNull('extra_data', instance.extraData); + val['members'] = instance.members?.map((e) => e?.toJson())?.toList(); + return val; +} diff --git a/packages/dart_client/lib/src/models/member.dart b/packages/dart_client/lib/src/models/member.dart new file mode 100644 index 00000000..511b7a7b --- /dev/null +++ b/packages/dart_client/lib/src/models/member.dart @@ -0,0 +1,96 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../models/user.dart'; + +part 'member.g.dart'; + +/// The class that contains the information about the user membership in a channel +@JsonSerializable() +class Member { + /// The interested user + final User user; + + /// The date in which the user accepted the invite to the channel + final DateTime inviteAcceptedAt; + + /// The date in which the user rejected the invite to the channel + final DateTime inviteRejectedAt; + + /// True if the user has been invited to the channel + final bool invited; + + /// The role of the user in the channel + final String role; + + /// The id of the interested user + final String userId; + + /// True if the user is a moderator of the channel + final bool isModerator; + + /// True if the member is banned from the channel + final bool banned; + + /// True if the member is shadow banned from the channel + final bool shadowBanned; + + /// The date of creation + final DateTime createdAt; + + /// The last date of update + final DateTime updatedAt; + + /// Constructor used for json serialization + Member({ + this.user, + this.inviteAcceptedAt, + this.inviteRejectedAt, + this.invited, + this.role, + this.userId, + this.isModerator, + this.createdAt, + this.updatedAt, + this.banned, + this.shadowBanned, + }); + + /// Create a new instance from a json + factory Member.fromJson(Map json) { + final member = _$MemberFromJson(json); + return member.copyWith( + userId: member.user?.id, + ); + } + + /// Creates a copy of [Member] with specified attributes overridden. + Member copyWith({ + User user, + DateTime inviteAcceptedAt, + DateTime inviteRejectedAt, + bool invited, + String role, + String userId, + bool isModerator, + DateTime createdAt, + DateTime updatedAt, + bool banned, + bool shadowBanned, + }) => + Member( + user: user ?? this.user, + inviteAcceptedAt: inviteAcceptedAt ?? this.inviteAcceptedAt, + inviteRejectedAt: inviteRejectedAt ?? this.inviteRejectedAt, + invited: invited ?? this.invited, + banned: banned ?? this.banned, + shadowBanned: shadowBanned ?? this.shadowBanned, + role: role ?? this.role, + userId: userId ?? this.userId, + isModerator: isModerator ?? this.isModerator, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + + /// Serialize to json + Map toJson() => _$MemberToJson(this); +} diff --git a/packages/dart_client/lib/src/models/member.g.dart b/packages/dart_client/lib/src/models/member.g.dart new file mode 100644 index 00000000..3ac8778e --- /dev/null +++ b/packages/dart_client/lib/src/models/member.g.dart @@ -0,0 +1,49 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'member.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Member _$MemberFromJson(Map json) { + return Member( + user: json['user'] == null + ? null + : User.fromJson((json['user'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + inviteAcceptedAt: json['invite_accepted_at'] == null + ? null + : DateTime.parse(json['invite_accepted_at'] as String), + inviteRejectedAt: json['invite_rejected_at'] == null + ? null + : DateTime.parse(json['invite_rejected_at'] as String), + invited: json['invited'] as bool, + role: json['role'] as String, + userId: json['user_id'] as String, + isModerator: json['is_moderator'] as bool, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + banned: json['banned'] as bool, + shadowBanned: json['shadow_banned'] as bool, + ); +} + +Map _$MemberToJson(Member instance) => { + 'user': instance.user?.toJson(), + 'invite_accepted_at': instance.inviteAcceptedAt?.toIso8601String(), + 'invite_rejected_at': instance.inviteRejectedAt?.toIso8601String(), + 'invited': instance.invited, + 'role': instance.role, + 'user_id': instance.userId, + 'is_moderator': instance.isModerator, + 'banned': instance.banned, + 'shadow_banned': instance.shadowBanned, + 'created_at': instance.createdAt?.toIso8601String(), + 'updated_at': instance.updatedAt?.toIso8601String(), + }; diff --git a/packages/dart_client/lib/src/models/message.dart b/packages/dart_client/lib/src/models/message.dart new file mode 100644 index 00000000..46f2551a --- /dev/null +++ b/packages/dart_client/lib/src/models/message.dart @@ -0,0 +1,278 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'attachment.dart'; +import 'reaction.dart'; +import 'serialization.dart'; +import 'user.dart'; + +part 'message.g.dart'; + +/// Enum defining the status of a sending message +enum MessageSendingStatus { + /// Message is being sent + SENDING, + + /// Message is being updated + UPDATING, + + /// Message is being deleted + DELETING, + + /// Message failed to send + FAILED, + + /// Message failed to updated + FAILED_UPDATE, + + /// Message failed to delete + FAILED_DELETE, + + /// Message correctly sent + SENT, +} + +/// The class that contains the information about a message +@JsonSerializable() +class Message { + /// The message ID. This is either created by Stream or set client side when the message is added. + final String id; + + /// The text of this message + final String text; + + /// The status of a sending message + @JsonKey(ignore: true) + final MessageSendingStatus status; + + /// The message type + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final String type; + + /// The list of attachments, either provided by the user or generated from a command or as a result of URL scraping. + @JsonKey(includeIfNull: false) + final List attachments; + + /// The list of user mentioned in the message + @JsonKey(toJson: Serialization.userIds) + final List mentionedUsers; + + /// A map describing the count of number of every reaction + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final Map reactionCounts; + + /// A map describing the count of score of every reaction + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final Map reactionScores; + + /// The latest reactions to the message created by any user. + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final List latestReactions; + + /// The reactions added to the message by the current user. + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final List ownReactions; + + /// The ID of the parent message, if the message is a reply. + final String parentId; + + /// A quoted reply message + @JsonKey(toJson: Serialization.readOnly) + final Message quotedMessage; + + /// The ID of the quoted message, if the message is a quoted reply. + final String quotedMessageId; + + /// Reserved field indicating the number of replies for this message. + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final int replyCount; + + /// Reserved field indicating the thread participants for this message. + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final List threadParticipants; + + /// Check if this message needs to show in the channel. + final bool showInChannel; + + /// If true the message is silent + final bool silent; + + /// If true the message is shadowed + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final bool shadowed; + + /// A used command name. + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final String command; + + /// Reserved field indicating when the message was created. + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final DateTime createdAt; + + /// Reserved field indicating when the message was updated last time. + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final DateTime updatedAt; + + /// User who sent the message + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final User user; + + /// Message custom extraData + @JsonKey(includeIfNull: false) + final Map extraData; + + /// True if the message is a system info + bool get isSystem => type == 'system'; + + /// True if the message has been deleted + bool get isDeleted => type == 'deleted'; + + /// True if the message is ephemeral + bool get isEphemeral => type == 'ephemeral'; + + /// Reserved field indicating when the message was deleted. + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final DateTime deletedAt; + + /// Known top level fields. + /// Useful for [Serialization] methods. + static const topLevelFields = [ + 'id', + 'text', + 'type', + 'silent', + 'attachments', + 'latest_reactions', + 'shadowed', + 'own_reactions', + 'mentioned_users', + 'reaction_counts', + 'reaction_scores', + 'silent', + 'parent_id', + 'quoted_message', + 'quoted_message_id', + 'reply_count', + 'thread_participants', + 'show_in_channel', + 'command', + 'created_at', + 'updated_at', + 'deleted_at', + 'user', + ]; + + /// Constructor used for json serialization + Message({ + this.id, + this.text, + this.type, + this.attachments, + this.mentionedUsers, + this.silent, + this.shadowed, + this.reactionCounts, + this.reactionScores, + this.latestReactions, + this.ownReactions, + this.parentId, + this.quotedMessage, + this.quotedMessageId, + this.replyCount = 0, + this.threadParticipants, + this.showInChannel, + this.command, + this.createdAt, + this.updatedAt, + this.user, + this.extraData, + this.deletedAt, + this.status = MessageSendingStatus.SENT, + }); + + /// Create a new instance from a json + factory Message.fromJson(Map json) => _$MessageFromJson( + Serialization.moveToExtraDataFromRoot(json, topLevelFields)); + + /// Serialize to json + Map toJson() => Serialization.moveFromExtraDataToRoot( + _$MessageToJson(this), topLevelFields); + + /// Creates a copy of [Message] with specified attributes overridden. + Message copyWith({ + String id, + String text, + String type, + List attachments, + List mentionedUsers, + Map reactionCounts, + Map reactionScores, + List latestReactions, + List ownReactions, + String parentId, + Message quotedMessage, + String quotedMessageId, + int replyCount, + List threadParticipants, + bool showInChannel, + bool shadowed, + bool silent, + String command, + DateTime createdAt, + DateTime updatedAt, + DateTime deletedAt, + User user, + Map extraData, + MessageSendingStatus status, + }) => + Message( + id: id ?? this.id, + text: text ?? this.text, + type: type ?? this.type, + attachments: attachments ?? this.attachments, + mentionedUsers: mentionedUsers ?? this.mentionedUsers, + reactionCounts: reactionCounts ?? this.reactionCounts, + reactionScores: reactionScores ?? this.reactionScores, + latestReactions: latestReactions ?? this.latestReactions, + ownReactions: ownReactions ?? this.ownReactions, + parentId: parentId ?? this.parentId, + quotedMessage: quotedMessage ?? this.quotedMessage, + quotedMessageId: quotedMessageId ?? this.quotedMessageId, + replyCount: replyCount ?? this.replyCount, + threadParticipants: threadParticipants ?? this.threadParticipants, + showInChannel: showInChannel ?? this.showInChannel, + command: command ?? this.command, + createdAt: createdAt ?? this.createdAt, + silent: silent ?? this.silent, + extraData: extraData ?? this.extraData, + user: user ?? this.user, + shadowed: shadowed ?? this.shadowed, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + status: status ?? this.status, + ); +} + +/// A translated message +/// It has an additional property called [i18n] +@JsonSerializable() +class TranslatedMessage extends Message { + /// Constructor used for json serialization + TranslatedMessage(this.i18n); + + /// A Map of + final Map i18n; + + /// Known top level fields. + /// Useful for [Serialization] methods. + static final topLevelFields = [ + 'i18n', + ...Message.topLevelFields, + ]; + + /// Create a new instance from a json + factory TranslatedMessage.fromJson(Map json) { + return _$TranslatedMessageFromJson( + Serialization.moveToExtraDataFromRoot(json, topLevelFields), + ); + } +} diff --git a/packages/dart_client/lib/src/models/message.g.dart b/packages/dart_client/lib/src/models/message.g.dart new file mode 100644 index 00000000..c30d8fe5 --- /dev/null +++ b/packages/dart_client/lib/src/models/message.g.dart @@ -0,0 +1,135 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'message.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Message _$MessageFromJson(Map json) { + return Message( + id: json['id'] as String, + text: json['text'] as String, + type: json['type'] as String, + attachments: (json['attachments'] as List) + ?.map((e) => e == null + ? null + : Attachment.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(), + mentionedUsers: (json['mentioned_users'] as List) + ?.map((e) => e == null + ? null + : User.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(), + silent: json['silent'] as bool, + shadowed: json['shadowed'] as bool, + reactionCounts: (json['reaction_counts'] as Map)?.map( + (k, e) => MapEntry(k as String, e as int), + ), + reactionScores: (json['reaction_scores'] as Map)?.map( + (k, e) => MapEntry(k as String, e as int), + ), + latestReactions: (json['latest_reactions'] as List) + ?.map((e) => e == null + ? null + : Reaction.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(), + ownReactions: (json['own_reactions'] as List) + ?.map((e) => e == null + ? null + : Reaction.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(), + parentId: json['parent_id'] as String, + quotedMessage: json['quoted_message'] == null + ? null + : Message.fromJson((json['quoted_message'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + quotedMessageId: json['quoted_message_id'] as String, + replyCount: json['reply_count'] as int, + threadParticipants: (json['thread_participants'] as List) + ?.map((e) => e == null + ? null + : User.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(), + showInChannel: json['show_in_channel'] as bool, + command: json['command'] as String, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + user: json['user'] == null + ? null + : User.fromJson((json['user'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + extraData: (json['extra_data'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + ), + deletedAt: json['deleted_at'] == null + ? null + : DateTime.parse(json['deleted_at'] as String), + ); +} + +Map _$MessageToJson(Message instance) { + final val = { + 'id': instance.id, + 'text': instance.text, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('type', readonly(instance.type)); + writeNotNull( + 'attachments', instance.attachments?.map((e) => e?.toJson())?.toList()); + val['mentioned_users'] = Serialization.userIds(instance.mentionedUsers); + writeNotNull('reaction_counts', readonly(instance.reactionCounts)); + writeNotNull('reaction_scores', readonly(instance.reactionScores)); + writeNotNull('latest_reactions', readonly(instance.latestReactions)); + writeNotNull('own_reactions', readonly(instance.ownReactions)); + val['parent_id'] = instance.parentId; + val['quoted_message'] = readonly(instance.quotedMessage); + val['quoted_message_id'] = instance.quotedMessageId; + writeNotNull('reply_count', readonly(instance.replyCount)); + writeNotNull('thread_participants', readonly(instance.threadParticipants)); + val['show_in_channel'] = instance.showInChannel; + val['silent'] = instance.silent; + writeNotNull('shadowed', readonly(instance.shadowed)); + writeNotNull('command', readonly(instance.command)); + writeNotNull('created_at', readonly(instance.createdAt)); + writeNotNull('updated_at', readonly(instance.updatedAt)); + writeNotNull('user', readonly(instance.user)); + writeNotNull('extra_data', instance.extraData); + writeNotNull('deleted_at', readonly(instance.deletedAt)); + return val; +} + +TranslatedMessage _$TranslatedMessageFromJson(Map json) { + return TranslatedMessage( + (json['i18n'] as Map)?.map( + (k, e) => MapEntry(k as String, e as String), + ), + ); +} + +Map _$TranslatedMessageToJson(TranslatedMessage instance) => + { + 'i18n': instance.i18n, + }; diff --git a/packages/dart_client/lib/src/models/mute.dart b/packages/dart_client/lib/src/models/mute.dart new file mode 100644 index 00000000..f33a9956 --- /dev/null +++ b/packages/dart_client/lib/src/models/mute.dart @@ -0,0 +1,36 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:stream_chat/src/models/channel_model.dart'; + +import 'serialization.dart'; +import 'user.dart'; + +part 'mute.g.dart'; + +/// The class that contains the information about a muted user +@JsonSerializable() +class Mute { + /// The user that performed the muting action + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final User user; + + /// The target user + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final ChannelModel channel; + + /// The date in which the use was muted + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final DateTime createdAt; + + /// The date of the last update + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final DateTime updatedAt; + + /// Constructor used for json serialization + Mute({this.user, this.channel, this.createdAt, this.updatedAt}); + + /// Create a new instance from a json + factory Mute.fromJson(Map json) => _$MuteFromJson(json); + + /// Serialize to json + Map toJson() => _$MuteToJson(this); +} diff --git a/packages/dart_client/lib/src/models/mute.g.dart b/packages/dart_client/lib/src/models/mute.g.dart new file mode 100644 index 00000000..9d0b9318 --- /dev/null +++ b/packages/dart_client/lib/src/models/mute.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'mute.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Mute _$MuteFromJson(Map json) { + return Mute( + user: json['user'] == null + ? null + : User.fromJson((json['user'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + channel: json['channel'] == null + ? null + : ChannelModel.fromJson((json['channel'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + ); +} + +Map _$MuteToJson(Mute instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('user', readonly(instance.user)); + writeNotNull('channel', readonly(instance.channel)); + writeNotNull('created_at', readonly(instance.createdAt)); + writeNotNull('updated_at', readonly(instance.updatedAt)); + return val; +} diff --git a/packages/dart_client/lib/src/models/own_user.dart b/packages/dart_client/lib/src/models/own_user.dart new file mode 100644 index 00000000..3246788c --- /dev/null +++ b/packages/dart_client/lib/src/models/own_user.dart @@ -0,0 +1,83 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'device.dart'; +import 'mute.dart'; +import 'serialization.dart'; +import 'user.dart'; + +part 'own_user.g.dart'; + +/// The class that defines the own user model +/// This object can be found in [Event] +@JsonSerializable() +class OwnUser extends User { + /// List of user devices + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final List devices; + + /// List of users muted by the user + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final List mutes; + + /// List of users muted by the user + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final List channelMutes; + + /// Total unread messages by the user + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final int totalUnreadCount; + + /// Total unread channels by the user + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final int unreadChannels; + + /// Known top level fields. + /// Useful for [Serialization] methods. + static final topLevelFields = [ + 'devices', + 'mutes', + 'total_unread_count', + 'unread_channels', + 'channel_mutes', + ...User.topLevelFields, + ]; + + /// Constructor used for json serialization + OwnUser({ + this.devices, + this.mutes, + this.totalUnreadCount, + this.unreadChannels, + this.channelMutes, + String id, + String role, + DateTime createdAt, + DateTime updatedAt, + DateTime lastActive, + bool online, + Map extraData, + bool banned, + }) : super( + id: id, + role: role, + createdAt: createdAt, + updatedAt: updatedAt, + lastActive: lastActive, + online: online, + extraData: extraData, + banned: banned, + ); + + /// Create a new instance from a json + factory OwnUser.fromJson(Map json) { + return _$OwnUserFromJson( + Serialization.moveToExtraDataFromRoot(json, topLevelFields)); + } + + /// Serialize to json + @override + Map toJson() { + return Serialization.moveFromExtraDataToRoot( + _$OwnUserToJson(this), topLevelFields); + } +} diff --git a/packages/dart_client/lib/src/models/own_user.g.dart b/packages/dart_client/lib/src/models/own_user.g.dart new file mode 100644 index 00000000..887e6b28 --- /dev/null +++ b/packages/dart_client/lib/src/models/own_user.g.dart @@ -0,0 +1,77 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'own_user.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +OwnUser _$OwnUserFromJson(Map json) { + return OwnUser( + devices: (json['devices'] as List) + ?.map((e) => e == null + ? null + : Device.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(), + mutes: (json['mutes'] as List) + ?.map((e) => e == null + ? null + : Mute.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(), + totalUnreadCount: json['total_unread_count'] as int, + unreadChannels: json['unread_channels'] as int, + channelMutes: (json['channel_mutes'] as List) + ?.map((e) => e == null + ? null + : Mute.fromJson((e as Map)?.map( + (k, e) => MapEntry(k as String, e), + ))) + ?.toList(), + id: json['id'] as String, + role: json['role'] as String, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + lastActive: json['last_active'] == null + ? null + : DateTime.parse(json['last_active'] as String), + online: json['online'] as bool, + extraData: (json['extra_data'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + ), + banned: json['banned'] as bool, + ); +} + +Map _$OwnUserToJson(OwnUser instance) { + final val = { + 'id': instance.id, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('role', readonly(instance.role)); + writeNotNull('created_at', readonly(instance.createdAt)); + writeNotNull('updated_at', readonly(instance.updatedAt)); + writeNotNull('last_active', readonly(instance.lastActive)); + writeNotNull('online', readonly(instance.online)); + writeNotNull('banned', readonly(instance.banned)); + writeNotNull('extra_data', instance.extraData); + writeNotNull('devices', readonly(instance.devices)); + writeNotNull('mutes', readonly(instance.mutes)); + writeNotNull('channel_mutes', readonly(instance.channelMutes)); + writeNotNull('total_unread_count', readonly(instance.totalUnreadCount)); + writeNotNull('unread_channels', readonly(instance.unreadChannels)); + return val; +} diff --git a/packages/dart_client/lib/src/models/reaction.dart b/packages/dart_client/lib/src/models/reaction.dart new file mode 100644 index 00000000..fb0c4fad --- /dev/null +++ b/packages/dart_client/lib/src/models/reaction.dart @@ -0,0 +1,68 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'serialization.dart'; +import 'user.dart'; + +part 'reaction.g.dart'; + +/// The class that defines a reaction +@JsonSerializable() +class Reaction { + /// The messageId to which the reaction belongs + final String messageId; + + /// The type of the reaction + final String type; + + /// The date of the reaction + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final DateTime createdAt; + + /// The user that sent the reaction + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final User user; + + /// The score of the reaction (ie. number of reactions sent) + final int score; + + /// The userId that sent the reaction + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final String userId; + + /// The score of the reaction (ie. number of reactions sent) + @JsonKey(includeIfNull: false) + final Map extraData; + + /// Map of custom user extraData + static const topLevelFields = [ + 'message_id', + 'created_at', + 'type', + 'user', + 'user_id', + 'score', + ]; + + /// Constructor used for json serialization + Reaction({ + this.messageId, + this.createdAt, + this.type, + this.user, + this.userId, + this.score, + this.extraData, + }); + + /// Create a new instance from a json + factory Reaction.fromJson(Map json) { + return _$ReactionFromJson( + Serialization.moveToExtraDataFromRoot(json, topLevelFields)); + } + + /// Serialize to json + Map toJson() { + return Serialization.moveFromExtraDataToRoot( + _$ReactionToJson(this), topLevelFields); + } +} diff --git a/packages/dart_client/lib/src/models/reaction.g.dart b/packages/dart_client/lib/src/models/reaction.g.dart new file mode 100644 index 00000000..a270af01 --- /dev/null +++ b/packages/dart_client/lib/src/models/reaction.g.dart @@ -0,0 +1,47 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'reaction.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Reaction _$ReactionFromJson(Map json) { + return Reaction( + messageId: json['message_id'] as String, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + type: json['type'] as String, + user: json['user'] == null + ? null + : User.fromJson((json['user'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + userId: json['user_id'] as String, + score: json['score'] as int, + extraData: (json['extra_data'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + ), + ); +} + +Map _$ReactionToJson(Reaction instance) { + final val = { + 'message_id': instance.messageId, + 'type': instance.type, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('created_at', readonly(instance.createdAt)); + writeNotNull('user', readonly(instance.user)); + val['score'] = instance.score; + writeNotNull('user_id', readonly(instance.userId)); + writeNotNull('extra_data', instance.extraData); + return val; +} diff --git a/packages/dart_client/lib/src/models/read.dart b/packages/dart_client/lib/src/models/read.dart new file mode 100644 index 00000000..ae85293d --- /dev/null +++ b/packages/dart_client/lib/src/models/read.dart @@ -0,0 +1,31 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'user.dart'; + +part 'read.g.dart'; + +/// The class that defines a read event +@JsonSerializable() +class Read { + /// Date of the read event + final DateTime lastRead; + + /// User who sent the event + final User user; + + /// Number of unread messages + final int unreadMessages; + + /// Constructor used for json serialization + Read({ + this.lastRead, + this.user, + this.unreadMessages, + }); + + /// Create a new instance from a json + factory Read.fromJson(Map json) => _$ReadFromJson(json); + + /// Serialize to json + Map toJson() => _$ReadToJson(this); +} diff --git a/packages/dart_client/lib/src/models/read.g.dart b/packages/dart_client/lib/src/models/read.g.dart new file mode 100644 index 00000000..d04ae146 --- /dev/null +++ b/packages/dart_client/lib/src/models/read.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'read.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Read _$ReadFromJson(Map json) { + return Read( + lastRead: json['last_read'] == null + ? null + : DateTime.parse(json['last_read'] as String), + user: json['user'] == null + ? null + : User.fromJson((json['user'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + )), + unreadMessages: json['unread_messages'] as int, + ); +} + +Map _$ReadToJson(Read instance) => { + 'last_read': instance.lastRead?.toIso8601String(), + 'user': instance.user?.toJson(), + 'unread_messages': instance.unreadMessages, + }; diff --git a/packages/dart_client/lib/src/models/serialization.dart b/packages/dart_client/lib/src/models/serialization.dart new file mode 100644 index 00000000..239ccb50 --- /dev/null +++ b/packages/dart_client/lib/src/models/serialization.dart @@ -0,0 +1,49 @@ +import 'user.dart'; + +/// Used to avoid to serialize properties to json +Null readonly(_) => null; + +/// Helper class for serialization to and from json +class Serialization { + /// Used to avoid to serialize properties to json + static const Function readOnly = readonly; + + /// List of users to list of userIds + static List userIds(List users) { + return users?.map((u) => u.id)?.toList(); + } + + /// Takes unknown json keys and puts them in the `extra_data` key + static Map moveToExtraDataFromRoot( + Map json, + List topLevelFields, + ) { + if (json == null) return null; + + final jsonClone = Map.from(json); + + final extraDataMap = Map.from(json) + ..removeWhere( + (key, value) => topLevelFields.contains(key), + ); + final rootFields = jsonClone + ..removeWhere((key, value) => extraDataMap.keys.contains(key)); + return rootFields + ..addAll({ + 'extra_data': extraDataMap, + }); + } + + /// Takes values in `extra_data` key and puts them on the root level of the json map + static Map moveFromExtraDataToRoot( + Map json, + List topLevelFields, + ) { + final jsonClone = Map.from(json); + return jsonClone + ..addAll({ + if (json['extra_data'] != null) ...json['extra_data'], + }) + ..remove('extra_data'); + } +} diff --git a/packages/dart_client/lib/src/models/user.dart b/packages/dart_client/lib/src/models/user.dart new file mode 100644 index 00000000..00ed1200 --- /dev/null +++ b/packages/dart_client/lib/src/models/user.dart @@ -0,0 +1,108 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'serialization.dart'; + +part 'user.g.dart'; + +/// The class that defines the user model +@JsonSerializable() +class User { + /// User id + final String id; + + /// User role + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final String role; + + /// User role + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final List teams; + + /// Date of user creation + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final DateTime createdAt; + + /// Date of last user update + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final DateTime updatedAt; + + /// Date of last user connection + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final DateTime lastActive; + + /// True if user is online + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final bool online; + + /// True if user is banned from the chat + @JsonKey(includeIfNull: false, toJson: Serialization.readOnly) + final bool banned; + + /// Map of custom user extraData + @JsonKey(includeIfNull: false) + final Map extraData; + + /// Known top level fields. + /// Useful for [Serialization] methods. + static const topLevelFields = [ + 'id', + 'role', + 'created_at', + 'updated_at', + 'last_active', + 'online', + 'banned', + 'teams', + ]; + + /// Use this named constructor to create a new user instance + User.init( + this.id, { + this.online, + this.extraData, + }) : createdAt = null, + updatedAt = null, + lastActive = null, + banned = null, + teams = null, + role = null; + + /// Constructor used for json serialization + User({ + this.id, + this.role, + this.createdAt, + this.updatedAt, + this.lastActive, + this.online, + this.extraData, + this.banned, + this.teams, + }); + + /// Shortcut for user name + String get name => + (extraData?.containsKey('name') == true && extraData['name'] != '') + ? extraData['name'] + : id; + + /// Create a new instance from a json + factory User.fromJson(Map json) { + return _$UserFromJson( + Serialization.moveToExtraDataFromRoot(json, topLevelFields)); + } + + /// Serialize to json + Map toJson() { + return Serialization.moveFromExtraDataToRoot( + _$UserToJson(this), topLevelFields); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is User && runtimeType == other.runtimeType && id == other.id; + + @override + int get hashCode => id.hashCode; +} diff --git a/packages/dart_client/lib/src/models/user.g.dart b/packages/dart_client/lib/src/models/user.g.dart new file mode 100644 index 00000000..b27935a7 --- /dev/null +++ b/packages/dart_client/lib/src/models/user.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +User _$UserFromJson(Map json) { + return User( + id: json['id'] as String, + role: json['role'] as String, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + lastActive: json['last_active'] == null + ? null + : DateTime.parse(json['last_active'] as String), + online: json['online'] as bool, + extraData: (json['extra_data'] as Map)?.map( + (k, e) => MapEntry(k as String, e), + ), + banned: json['banned'] as bool, + teams: (json['teams'] as List)?.map((e) => e as String)?.toList(), + ); +} + +Map _$UserToJson(User instance) { + final val = { + 'id': instance.id, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('role', readonly(instance.role)); + writeNotNull('teams', readonly(instance.teams)); + writeNotNull('created_at', readonly(instance.createdAt)); + writeNotNull('updated_at', readonly(instance.updatedAt)); + writeNotNull('last_active', readonly(instance.lastActive)); + writeNotNull('online', readonly(instance.online)); + writeNotNull('banned', readonly(instance.banned)); + writeNotNull('extra_data', instance.extraData); + return val; +} diff --git a/packages/dart_client/lib/src/notifications.dart b/packages/dart_client/lib/src/notifications.dart new file mode 100644 index 00000000..2fc500cf --- /dev/null +++ b/packages/dart_client/lib/src/notifications.dart @@ -0,0 +1,119 @@ +import 'dart:convert'; + +import 'package:logging/logging.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:stream_chat/src/api/responses.dart'; +import 'package:stream_chat/src/db/offline_storage.dart'; +import 'package:stream_chat/src/models/channel_model.dart'; +import 'package:stream_chat/src/models/channel_state.dart'; +import 'package:stream_chat/src/models/message.dart'; + +import 'client.dart'; +import 'models/own_user.dart'; + +/// Utility class to handle and show notifications +class NotificationService { + static Future _handleNotification( + Message message, + ChannelModel channelModel, + Client client, + ) async { + if (message != null && client.persistenceEnabled) { + if (client?.state?.channels == null) { + final sharedPreferences = await _getSharedPreferences(); + final userId = sharedPreferences.getString(KEY_USER_ID); + + final offlineStorage = OfflineStorage(userId, Logger('💽')); + await offlineStorage.updateChannelState( + ChannelState( + channel: channelModel, + messages: [message], + ), + ); + await offlineStorage.disconnect(); + } else { + final channel = client.state.channels[channelModel.cid]; + channel.state.updateChannelState( + ChannelState( + channel: channelModel, + messages: [message], + ), + ); + } + } + } + + static SharedPreferences _sharedPreferences; + static Future _getSharedPreferences() async { + _sharedPreferences ??= await SharedPreferences.getInstance(); + return _sharedPreferences; + } + + /// Gets the message using the client without storing it in the offline storage + /// It returns an object containing the information about the message and the channel + static Future getMessage(String messageId) async { + final sharedPreferences = await _getSharedPreferences(); + final apiKey = sharedPreferences.getString(KEY_API_KEY); + + final client = Client( + apiKey, + persistenceEnabled: false, + ); + final userId = sharedPreferences.getString(KEY_USER_ID); + final token = sharedPreferences.getString(KEY_TOKEN); + + client.state.user = OwnUser(id: userId); + client.token = token; + + final res = await client.getMessage(messageId); + return res; + } + + /// Stores the message in the offline storage + static Future storeMessage(GetMessageResponse messageResponse) async { + final sharedPreferences = await _getSharedPreferences(); + final userId = sharedPreferences.getString(KEY_USER_ID); + + final offlineStorage = OfflineStorage( + userId, + Logger('💽'), + ); + + await offlineStorage.updateChannelState(ChannelState( + messages: [messageResponse.message], + channel: messageResponse.channel, + )); + + await offlineStorage.disconnect(); + } + + /// Gets the message using the client and stores it in the offline storage + /// It returns an object containing the information about the message and the channel + static Future getAndStoreMessage(String messageId) async { + final getMessageResponse = await getMessage(messageId); + await storeMessage(getMessageResponse); + return getMessageResponse; + } + + /// Handles the ios message queue generated by the Notification Service Extension + static Future handleIosMessageQueue(Client client) async { + final sharedPreferences = await SharedPreferences.getInstance(); + await sharedPreferences.reload(); + final messageQueue = sharedPreferences.getStringList('messageQueue'); + if (messageQueue != null) { + messageQueue.forEach((m) { + final jsonMessage = jsonDecode(m); + + final message = Message.fromJson(jsonMessage); + final channelModel = ChannelModel.fromJson(jsonMessage['channel']); + + _handleNotification( + message, + channelModel, + client, + ); + }); + await sharedPreferences.remove('messageQueue'); + } + } +} diff --git a/packages/dart_client/lib/stream_chat.dart b/packages/dart_client/lib/stream_chat.dart new file mode 100644 index 00000000..101e9440 --- /dev/null +++ b/packages/dart_client/lib/stream_chat.dart @@ -0,0 +1,29 @@ +library stream_chat; + +export 'package:dio/src/dio_error.dart'; +export 'package:dio/src/multipart_file.dart'; +export 'package:logging/src/level.dart'; + +export './src/api/channel.dart'; +export './src/api/connection_status.dart'; +export './src/api/requests.dart'; +export './src/api/requests.dart'; +export './src/api/responses.dart'; +export './src/client.dart'; +export './src/event_type.dart'; +export './src/models/action.dart'; +export './src/models/attachment.dart'; +export './src/models/channel_config.dart'; +export './src/models/channel_model.dart'; +export './src/models/channel_state.dart'; +export './src/models/command.dart'; +export './src/models/device.dart'; +export './src/models/event.dart'; +export './src/models/member.dart'; +export './src/models/message.dart'; +export './src/models/mute.dart'; +export './src/models/own_user.dart'; +export './src/models/reaction.dart'; +export './src/models/read.dart'; +export './src/models/user.dart'; +export './src/notifications.dart'; diff --git a/packages/dart_client/lib/version.dart b/packages/dart_client/lib/version.dart new file mode 100644 index 00000000..75bf93d6 --- /dev/null +++ b/packages/dart_client/lib/version.dart @@ -0,0 +1,5 @@ +import 'package:stream_chat/src/client.dart'; + +/// Current package version +/// Used in [Client] to build the `x-stream-client` header +const PACKAGE_VERSION = '0.2.23+1'; diff --git a/packages/dart_client/peanut.yaml b/packages/dart_client/peanut.yaml new file mode 100644 index 00000000..97d20f52 --- /dev/null +++ b/packages/dart_client/peanut.yaml @@ -0,0 +1,3 @@ +# Configuration for https://pub.dev/packages/peanut +directories: + - example/web diff --git a/packages/dart_client/pubspec.yaml b/packages/dart_client/pubspec.yaml new file mode 100644 index 00000000..2c28136d --- /dev/null +++ b/packages/dart_client/pubspec.yaml @@ -0,0 +1,36 @@ +name: stream_chat +homepage: https://getstream.io/ +description: The official Dart client for Stream Chat, a service for building chat applications. +version: 0.2.23+1 +repository: https://github.com/GetStream/stream-chat-dart +issue_tracker: https://github.com/GetStream/stream-chat-dart/issues + +environment: + sdk: ">=2.2.2 <3.0.0" + +dependencies: + flutter: + sdk: flutter + json_annotation: ^3.0.1 + shared_preferences: ^0.5.7+3 + logging: ^0.11.4 + dio: ^3.0.10 + web_socket_channel: ^1.1.0 + uuid: ^2.2.2 + async: ^2.4.1 + stream_channel: ^2.0.0 + moor: ^3.3.1 + path_provider: ^1.6.10 + path: ^1.6.4 + rxdart: ^0.24.1 + collection: ^1.14.12 + sqlite3_flutter_libs: ^0.3.0 + +dev_dependencies: + build_runner: ^1.10.0 + json_serializable: ^3.3.0 + moor_generator: ^3.1.0 + flutter_test: + sdk: flutter + pedantic: ^1.8.0+1 + mockito: ^4.1.1 diff --git a/packages/dart_client/test/src/api/channel_test.dart b/packages/dart_client/test/src/api/channel_test.dart new file mode 100644 index 00000000..d83d6f12 --- /dev/null +++ b/packages/dart_client/test/src/api/channel_test.dart @@ -0,0 +1,2099 @@ +import 'package:dio/dio.dart'; +import 'package:dio/native_imp.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat/src/api/requests.dart'; +import 'package:stream_chat/src/client.dart'; +import 'package:stream_chat/src/event_type.dart'; +import 'package:stream_chat/src/models/event.dart'; +import 'package:stream_chat/src/models/message.dart'; +import 'package:stream_chat/src/models/reaction.dart'; + +class MockDio extends Mock implements DioForNative {} + +class MockHttpClientAdapter extends Mock implements HttpClientAdapter {} + +void main() { + group('src/api/channel', () { + group('message', () { + test('sendMessage', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + final message = Message( + text: 'hey', + id: 'test', + ); + + when(mockDio.post('/channels/messaging/testid/message', data: { + 'message': message.toJson(), + })).thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.sendMessage(message); + + verify( + mockDio.post('/channels/messaging/testid/message', data: { + 'message': message.toJson(), + })).called(1); + }); + + test('markRead', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + + final channelClient = client.channel('messaging', id: 'testid'); + + when(mockDio.post( + any, + data: anyNamed('data'), + )).thenAnswer((_) async => Response( + data: '{}', + statusCode: 200, + )); + await channelClient.watch(); + + when(mockDio.post('/channels/messaging/testid/read', data: {})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.markRead(); + + verify(mockDio.post('/channels/messaging/testid/read', + data: {})).called(1); + }); + + test('getReplies', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + final pagination = PaginationParams(); + + when(mockDio.get('/messages/messageid/replies', + queryParameters: pagination.toJson())) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.getReplies('messageid', pagination); + + verify(mockDio.get('/messages/messageid/replies', + queryParameters: pagination.toJson())) + .called(1); + }); + + test('sendAction', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + + when(mockDio.post( + any, + data: anyNamed('data'), + )).thenAnswer((_) async => Response( + data: '{}', + statusCode: 200, + )); + await channelClient.watch(); + + final Map data = {'test': true}; + + when(mockDio.post('/messages/messageid/action', data: { + 'id': 'testid', + 'type': 'messaging', + 'form_data': data, + 'message_id': 'messageid', + })).thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.sendAction(Message(id: 'messageid'), data); + + verify(mockDio.post('/messages/messageid/action', data: { + 'id': 'testid', + 'type': 'messaging', + 'form_data': data, + 'message_id': 'messageid', + })).called(1); + }); + + test('getMessagesById', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + final messageIds = ['a', 'b']; + + when(mockDio.get('/channels/messaging/testid/messages', + queryParameters: {'ids': messageIds.join(',')})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.getMessagesById(messageIds); + + verify(mockDio.get('/channels/messaging/testid/messages', + queryParameters: {'ids': messageIds.join(',')})).called(1); + }); + + test('sendFile', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + final file = MultipartFile.fromString('file'); + + when(mockDio.post('/channels/messaging/testid/file', + data: argThat(isA(), named: 'data'))) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.sendFile(file); + + verify(mockDio.post('/channels/messaging/testid/file', + data: argThat(isA(), named: 'data'))) + .called(1); + }); + + test('sendImage', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + final file = MultipartFile.fromString('file'); + + when(mockDio.post('/channels/messaging/testid/image', + data: argThat(isA(), named: 'data'))) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.sendImage(file); + + verify(mockDio.post('/channels/messaging/testid/image', + data: argThat(isA(), named: 'data'))) + .called(1); + }); + + test('deleteFile', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + final url = 'url'; + + when(mockDio.delete('/channels/messaging/testid/file', + queryParameters: {'url': url})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.deleteFile(url); + + verify(mockDio.delete('/channels/messaging/testid/file', + queryParameters: {'url': url})).called(1); + }); + + test('deleteImage', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + final url = 'url'; + + when(mockDio.delete('/channels/messaging/testid/image', + queryParameters: {'url': url})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.deleteImage(url); + + verify(mockDio.delete('/channels/messaging/testid/image', + queryParameters: {'url': url})).called(1); + }); + }); + + test('sendEvent', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + + when(mockDio.post( + any, + data: anyNamed('data'), + )).thenAnswer((_) async => Response( + data: '{}', + statusCode: 200, + )); + await channelClient.watch(); + + final event = Event(type: EventType.any); + + when(mockDio.post('/channels/messaging/testid/event', + data: {'event': event.toJson()})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.sendEvent(event); + + verify(mockDio.post('/channels/messaging/testid/event', + data: {'event': event.toJson()})).called(1); + }); + + test('keyStroke', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + + when(mockDio.post( + any, + data: anyNamed('data'), + )).thenAnswer((_) async => Response( + data: '{}', + statusCode: 200, + )); + await channelClient.watch(); + + final event = Event(type: EventType.typingStart); + + when(mockDio.post('/channels/messaging/testid/event', + data: {'event': event.toJson()})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.keyStroke(); + + verify(mockDio.post('/channels/messaging/testid/event', + data: {'event': event.toJson()})).called(1); + }); + + test('stopTyping', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + + when(mockDio.post( + any, + data: anyNamed('data'), + )).thenAnswer((_) async => Response( + data: '{}', + statusCode: 200, + )); + await channelClient.watch(); + + final event = Event(type: EventType.typingStop); + + when(mockDio.post('/channels/messaging/testid/event', + data: {'event': event.toJson()})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.stopTyping(); + + verify(mockDio.post('/channels/messaging/testid/event', + data: {'event': event.toJson()})).called(1); + }); + + group('reactions', () { + test('sendReaction', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + final reactionType = 'test'; + + when(mockDio.post( + '/messages/messageid/reaction', + data: { + 'reaction': { + 'type': reactionType, + }, + 'enforce_unique': false, + }, + )).thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.sendReaction( + Message( + id: 'messageid', + ), + reactionType, + ); + + verify(mockDio.post('/messages/messageid/reaction', data: { + 'reaction': { + 'type': reactionType, + }, + 'enforce_unique': false, + })).called(1); + }); + + test('deleteReaction', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + + when(mockDio.post( + any, + data: anyNamed('data'), + )).thenAnswer((_) async => Response( + data: '{}', + statusCode: 200, + )); + await channelClient.watch(); + + when(mockDio.delete('/messages/messageid/reaction/test')) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.deleteReaction( + Message(id: 'messageid'), + Reaction(type: 'test'), + ); + + verify(mockDio.delete('/messages/messageid/reaction/test')) + .called(1); + }); + + test('getReactions', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + final pagination = PaginationParams(); + + when(mockDio.get('/messages/messageid/reactions', + queryParameters: pagination.toJson())) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.getReactions('messageid', pagination); + + verify(mockDio.get('/messages/messageid/reactions', + queryParameters: pagination.toJson())) + .called(1); + }); + }); + + group('channel', () { + test('addMembers', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + final members = ['vishal']; + final message = Message(text: 'test'); + + when(mockDio.post('/channels/messaging/testid', + data: {'add_members': members, 'message': message.toJson()})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.addMembers(members, message); + + verify(mockDio.post('/channels/messaging/testid', + data: {'add_members': members, 'message': message.toJson()})) + .called(1); + }); + + test('acceptInvite', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + final message = Message(text: 'test'); + + when(mockDio.post('/channels/messaging/testid', + data: {'accept_invite': true, 'message': message.toJson()})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.acceptInvite(message); + + verify(mockDio.post('/channels/messaging/testid', + data: {'accept_invite': true, 'message': message.toJson()})) + .called(1); + }); + + group('query', () { + test('without id', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging'); + final Map options = { + 'watch': true, + 'state': false, + 'presence': true, + }; + + when(mockDio.post('/channels/messaging/query', data: options)) + .thenAnswer((_) async { + return Response(data: r''' + { + "channel": { + "id": "!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "type": "messaging", + "cid": "messaging:!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "last_message_at": "2020-01-28T22:17:31.204287Z", + "created_at": "2020-01-28T22:17:31.00187Z", + "updated_at": "2020-01-28T22:17:31.00187Z", + "created_by": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "frozen": false, + "member_count": 2, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "Mia Denys", + "image": "https://randomuser.me/api/portraits/women/2.jpg" + }, + "messages": [ + { + "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "The Lion King Disney GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "text": "Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA" + } + ], + "latest_reactions": [ + { + "message_id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "type": "love", + "score": 1, + "created_at": "2020-01-28T22:17:31.128376Z", + "updated_at": "2020-01-28T22:17:31.128376Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "love": 1 + }, + "reaction_scores": { + "love": 1 + }, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.107978Z", + "updated_at": "2020-01-28T22:17:31.130506Z", + "mentioned_users": [] + }, + { + "id": "16e19c46-fb96-4f89-8031-d5bd2ce3bd64", + "text": "Few can name a topfull mother that isn't a breezeless damage.", + "html": "\u003cp\u003eFew can name a topfull mother that isn’t a breezeless damage.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.153518Z", + "updated_at": "2020-01-28T22:17:31.153518Z", + "mentioned_users": [] + }, + { + "id": "38b1b252-c9a6-4aea-a39e-8de3b7f7604b", + "text": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "Moustache Thumbs Up GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "text": "Discover \u0026 share this Pouce Leve GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "thumb_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "asset_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.155428Z", + "updated_at": "2020-01-28T22:17:31.155428Z", + "mentioned_users": [] + }, + { + "id": "5c7d0c68-72d5-4027-b6b7-711b342d0fc4", + "text": "The carbons could be said to resemble smartish hoods.", + "html": "\u003cp\u003eThe carbons could be said to resemble smartish hoods.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.157811Z", + "updated_at": "2020-01-28T22:17:31.157811Z", + "mentioned_users": [] + }, + { + "id": "5b02535a-0c3c-45fa-9cf8-16f840c5123f", + "text": "Their software was, in this moment, a prolix feature.", + "html": "\u003cp\u003eTheir software was, in this moment, a prolix feature.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.158391Z", + "updated_at": "2020-01-28T22:17:31.158391Z", + "mentioned_users": [] + } + ], + "watcher_count": 1, + "read": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "last_read": "2020-01-28T22:17:31.016937728Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "last_read": "2020-01-28T22:17:31.018856448Z" + } + ], + "members": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "role": "member", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "role": "owner", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + } + ] + } + ''', statusCode: 200); + }); + + final response = await channelClient.query(options: options); + + verify(mockDio.post('/channels/messaging/query', + data: options)) + .called(1); + expect(channelClient.id, response.channel.id); + expect(channelClient.cid, response.channel.cid); + }); + + test('with id', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + final Map options = { + 'state': false, + }; + + when(mockDio.post('/channels/messaging/testid/query', + data: options)) + .thenAnswer((_) async => Response(data: r''' + { + "channel": { + "id": "!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "type": "messaging", + "cid": "messaging:!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "last_message_at": "2020-01-28T22:17:31.204287Z", + "created_at": "2020-01-28T22:17:31.00187Z", + "updated_at": "2020-01-28T22:17:31.00187Z", + "created_by": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "frozen": false, + "member_count": 2, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "Mia Denys", + "image": "https://randomuser.me/api/portraits/women/2.jpg" + }, + "messages": [ + { + "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "The Lion King Disney GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "text": "Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA" + } + ], + "latest_reactions": [ + { + "message_id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "type": "love", + "score": 1, + "created_at": "2020-01-28T22:17:31.128376Z", + "updated_at": "2020-01-28T22:17:31.128376Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "love": 1 + }, + "reaction_scores": { + "love": 1 + }, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.107978Z", + "updated_at": "2020-01-28T22:17:31.130506Z", + "mentioned_users": [] + }, + { + "id": "16e19c46-fb96-4f89-8031-d5bd2ce3bd64", + "text": "Few can name a topfull mother that isn't a breezeless damage.", + "html": "\u003cp\u003eFew can name a topfull mother that isn’t a breezeless damage.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.153518Z", + "updated_at": "2020-01-28T22:17:31.153518Z", + "mentioned_users": [] + }, + { + "id": "38b1b252-c9a6-4aea-a39e-8de3b7f7604b", + "text": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "Moustache Thumbs Up GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "text": "Discover \u0026 share this Pouce Leve GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "thumb_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "asset_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.155428Z", + "updated_at": "2020-01-28T22:17:31.155428Z", + "mentioned_users": [] + }, + { + "id": "5c7d0c68-72d5-4027-b6b7-711b342d0fc4", + "text": "The carbons could be said to resemble smartish hoods.", + "html": "\u003cp\u003eThe carbons could be said to resemble smartish hoods.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.157811Z", + "updated_at": "2020-01-28T22:17:31.157811Z", + "mentioned_users": [] + }, + { + "id": "5b02535a-0c3c-45fa-9cf8-16f840c5123f", + "text": "Their software was, in this moment, a prolix feature.", + "html": "\u003cp\u003eTheir software was, in this moment, a prolix feature.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.158391Z", + "updated_at": "2020-01-28T22:17:31.158391Z", + "mentioned_users": [] + } + ], + "watcher_count": 1, + "read": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "last_read": "2020-01-28T22:17:31.016937728Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "last_read": "2020-01-28T22:17:31.018856448Z" + } + ], + "members": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "role": "member", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "role": "owner", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + } + ] + } + ''', statusCode: 200)); + + await channelClient.query(options: options); + + verify(mockDio.post('/channels/messaging/testid/query', + data: options)) + .called(1); + }); + }); + + test('create', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging'); + final Map options = { + 'watch': false, + 'state': false, + 'presence': false, + }; + + when(mockDio.post('/channels/messaging/query', data: options)) + .thenAnswer((_) async => Response(data: r''' + { + "channel": { + "id": "!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "type": "messaging", + "cid": "messaging:!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "last_message_at": "2020-01-28T22:17:31.204287Z", + "created_at": "2020-01-28T22:17:31.00187Z", + "updated_at": "2020-01-28T22:17:31.00187Z", + "created_by": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "frozen": false, + "member_count": 2, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "Mia Denys", + "image": "https://randomuser.me/api/portraits/women/2.jpg" + }, + "messages": [ + { + "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "The Lion King Disney GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "text": "Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA" + } + ], + "latest_reactions": [ + { + "message_id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "type": "love", + "score": 1, + "created_at": "2020-01-28T22:17:31.128376Z", + "updated_at": "2020-01-28T22:17:31.128376Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "love": 1 + }, + "reaction_scores": { + "love": 1 + }, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.107978Z", + "updated_at": "2020-01-28T22:17:31.130506Z", + "mentioned_users": [] + }, + { + "id": "16e19c46-fb96-4f89-8031-d5bd2ce3bd64", + "text": "Few can name a topfull mother that isn't a breezeless damage.", + "html": "\u003cp\u003eFew can name a topfull mother that isn’t a breezeless damage.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.153518Z", + "updated_at": "2020-01-28T22:17:31.153518Z", + "mentioned_users": [] + }, + { + "id": "38b1b252-c9a6-4aea-a39e-8de3b7f7604b", + "text": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "Moustache Thumbs Up GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "text": "Discover \u0026 share this Pouce Leve GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "thumb_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "asset_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.155428Z", + "updated_at": "2020-01-28T22:17:31.155428Z", + "mentioned_users": [] + }, + { + "id": "5c7d0c68-72d5-4027-b6b7-711b342d0fc4", + "text": "The carbons could be said to resemble smartish hoods.", + "html": "\u003cp\u003eThe carbons could be said to resemble smartish hoods.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.157811Z", + "updated_at": "2020-01-28T22:17:31.157811Z", + "mentioned_users": [] + }, + { + "id": "5b02535a-0c3c-45fa-9cf8-16f840c5123f", + "text": "Their software was, in this moment, a prolix feature.", + "html": "\u003cp\u003eTheir software was, in this moment, a prolix feature.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.158391Z", + "updated_at": "2020-01-28T22:17:31.158391Z", + "mentioned_users": [] + } + ], + "watcher_count": 1, + "read": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "last_read": "2020-01-28T22:17:31.016937728Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "last_read": "2020-01-28T22:17:31.018856448Z" + } + ], + "members": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "role": "member", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "role": "owner", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + } + ] + } + ''', statusCode: 200)); + + final response = await channelClient.create(); + + verify(mockDio.post('/channels/messaging/query', data: options)) + .called(1); + expect(channelClient.id, response.channel.id); + expect(channelClient.cid, response.channel.cid); + }); + + test('watch', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging'); + final options = { + 'watch': true, + 'state': true, + 'presence': true, + }; + + when(mockDio.post('/channels/messaging/query', data: options)) + .thenAnswer((_) async => Response(data: r''' + { + "channel": { + "id": "!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "type": "messaging", + "cid": "messaging:!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "last_message_at": "2020-01-28T22:17:31.204287Z", + "created_at": "2020-01-28T22:17:31.00187Z", + "updated_at": "2020-01-28T22:17:31.00187Z", + "created_by": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "frozen": false, + "member_count": 2, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "Mia Denys", + "image": "https://randomuser.me/api/portraits/women/2.jpg" + }, + "messages": [ + { + "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "The Lion King Disney GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "text": "Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA" + } + ], + "latest_reactions": [ + { + "message_id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "type": "love", + "score": 1, + "created_at": "2020-01-28T22:17:31.128376Z", + "updated_at": "2020-01-28T22:17:31.128376Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "love": 1 + }, + "reaction_scores": { + "love": 1 + }, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.107978Z", + "updated_at": "2020-01-28T22:17:31.130506Z", + "mentioned_users": [] + }, + { + "id": "16e19c46-fb96-4f89-8031-d5bd2ce3bd64", + "text": "Few can name a topfull mother that isn't a breezeless damage.", + "html": "\u003cp\u003eFew can name a topfull mother that isn’t a breezeless damage.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.153518Z", + "updated_at": "2020-01-28T22:17:31.153518Z", + "mentioned_users": [] + }, + { + "id": "38b1b252-c9a6-4aea-a39e-8de3b7f7604b", + "text": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "Moustache Thumbs Up GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "text": "Discover \u0026 share this Pouce Leve GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "thumb_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "asset_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.155428Z", + "updated_at": "2020-01-28T22:17:31.155428Z", + "mentioned_users": [] + }, + { + "id": "5c7d0c68-72d5-4027-b6b7-711b342d0fc4", + "text": "The carbons could be said to resemble smartish hoods.", + "html": "\u003cp\u003eThe carbons could be said to resemble smartish hoods.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.157811Z", + "updated_at": "2020-01-28T22:17:31.157811Z", + "mentioned_users": [] + }, + { + "id": "5b02535a-0c3c-45fa-9cf8-16f840c5123f", + "text": "Their software was, in this moment, a prolix feature.", + "html": "\u003cp\u003eTheir software was, in this moment, a prolix feature.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.158391Z", + "updated_at": "2020-01-28T22:17:31.158391Z", + "mentioned_users": [] + } + ], + "watcher_count": 1, + "read": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "last_read": "2020-01-28T22:17:31.016937728Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "last_read": "2020-01-28T22:17:31.018856448Z" + } + ], + "members": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "role": "member", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "role": "owner", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + } + ] + } + ''', statusCode: 200)); + + final response = await channelClient.watch({'presence': true}); + + verify(mockDio.post('/channels/messaging/query', data: options)) + .called(1); + expect(channelClient.id, response.channel.id); + expect(channelClient.cid, response.channel.cid); + }); + + test('stopWatching', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + + when(mockDio.post( + '/channels/messaging/testid/stop-watching', + data: {}, + )).thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.stopWatching(); + + verify(mockDio.post( + '/channels/messaging/testid/stop-watching', + data: {}, + )).called(1); + }); + + test('update', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + final message = Message(text: 'test'); + + when(mockDio.post('/channels/messaging/testid', data: { + 'message': message.toJson(), + 'data': {'test': true}, + })).thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.update({'test': true}, message); + + verify(mockDio.post('/channels/messaging/testid', data: { + 'message': message.toJson(), + 'data': {'test': true}, + })).called(1); + }); + + test('delete', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + + when(mockDio.delete('/channels/messaging/testid')) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.delete(); + + verify(mockDio.delete('/channels/messaging/testid')).called(1); + }); + + test('truncate', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + + when(mockDio.post('/channels/messaging/testid/truncate')) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.truncate(); + + verify(mockDio.post('/channels/messaging/testid/truncate')) + .called(1); + }); + + test('rejectInvite', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + final message = Message(text: 'test'); + + when(mockDio.post('/channels/messaging/testid', + data: {'reject_invite': true, 'message': message.toJson()})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.rejectInvite(message); + + verify(mockDio.post('/channels/messaging/testid', + data: {'reject_invite': true, 'message': message.toJson()})) + .called(1); + }); + + test('inviteMembers', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + final members = ['vishal']; + final message = Message(text: 'test'); + + when(mockDio.post('/channels/messaging/testid', + data: {'invites': members, 'message': message.toJson()})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.inviteMembers(members, message); + + verify(mockDio.post('/channels/messaging/testid', + data: {'invites': members, 'message': message.toJson()})).called(1); + }); + + test('removeMembers', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + final members = ['vishal']; + final message = Message(text: 'test'); + + when(mockDio.post('/channels/messaging/testid', + data: {'remove_members': members, 'message': message.toJson()})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.removeMembers(members, message); + + verify(mockDio.post('/channels/messaging/testid', + data: {'remove_members': members, 'message': message.toJson()})) + .called(1); + }); + + test('hide', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + + when(mockDio.post( + any, + data: anyNamed('data'), + )).thenAnswer((_) async => Response( + data: '{}', + statusCode: 200, + )); + await channelClient.watch(); + + when(mockDio.post('/channels/messaging/testid/hide', + data: {'clear_history': true})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.hide(clearHistory: true); + + verify(mockDio.post('/channels/messaging/testid/hide', + data: {'clear_history': true})).called(1); + }); + + test('show', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + + when(mockDio.post( + any, + data: anyNamed('data'), + )).thenAnswer((_) async => Response( + data: '{}', + statusCode: 200, + )); + await channelClient.watch(); + + when(mockDio.post('/channels/messaging/testid/show')) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.show(); + + verify(mockDio.post('/channels/messaging/testid/show')) + .called(1); + }); + + test('banUser', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + + when(mockDio.post( + any, + data: anyNamed('data'), + )).thenAnswer((_) async => Response( + data: '{}', + statusCode: 200, + )); + await channelClient.watch(); + + when(mockDio.post('/moderation/ban', data: { + 'test': true, + 'target_user_id': 'test-id', + 'type': 'messaging', + 'id': 'testid', + })).thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + final Map options = {'test': true}; + await channelClient.banUser('test-id', options); + + verify(mockDio.post('/moderation/ban', data: { + 'test': true, + 'target_user_id': 'test-id', + 'type': 'messaging', + 'id': 'testid', + })).called(1); + }); + + test('unbanUser', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + tokenProvider: (_) async => '', + ); + final channelClient = client.channel('messaging', id: 'testid'); + + when(mockDio.post( + any, + data: anyNamed('data'), + )).thenAnswer((_) async => Response( + data: '{}', + statusCode: 200, + )); + await channelClient.watch(); + + when(mockDio.delete('/moderation/ban', queryParameters: { + 'target_user_id': 'test-id', + 'type': 'messaging', + 'id': 'testid', + })).thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await channelClient.unbanUser('test-id'); + + verify(mockDio.delete('/moderation/ban', queryParameters: { + 'target_user_id': 'test-id', + 'type': 'messaging', + 'id': 'testid', + })).called(1); + }); + }); + }); +} diff --git a/packages/dart_client/test/src/api/requests_test.dart b/packages/dart_client/test/src/api/requests_test.dart new file mode 100644 index 00000000..3ad762a7 --- /dev/null +++ b/packages/dart_client/test/src/api/requests_test.dart @@ -0,0 +1,18 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/stream_chat.dart'; + +void main() { + group('src/api/requests', () { + test('SortOption', () { + final option = SortOption('name'); + final j = option.toJson(); + expect(j, {'field': 'name', 'direction': -1}); + }); + + test('PaginationParams', () { + final option = PaginationParams(); + final j = option.toJson(); + expect(j, {'limit': 10}); + }); + }); +} diff --git a/packages/dart_client/test/src/api/responses_test.dart b/packages/dart_client/test/src/api/responses_test.dart new file mode 100644 index 00000000..df576ba4 --- /dev/null +++ b/packages/dart_client/test/src/api/responses_test.dart @@ -0,0 +1,4336 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/src/api/responses.dart'; +import 'package:stream_chat/src/models/device.dart'; +import 'package:stream_chat/src/models/member.dart'; +import 'package:stream_chat/src/models/message.dart'; +import 'package:stream_chat/src/models/reaction.dart'; +import 'package:stream_chat/src/models/read.dart'; +import 'package:stream_chat/stream_chat.dart'; + +void main() { + group('src/api/responses', () { + test('QueryChannelsResponse', () { + const jsonExample = r'''{ + "channels": [ + { + "channel": { + "id": "!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "type": "messaging", + "cid": "messaging:!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "last_message_at": "2020-01-28T22:17:31.204287Z", + "created_at": "2020-01-28T22:17:31.00187Z", + "updated_at": "2020-01-28T22:17:31.00187Z", + "created_by": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "frozen": false, + "member_count": 2, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "Mia Denys", + "image": "https://randomuser.me/api/portraits/women/2.jpg" + }, + "messages": [ + { + "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "The Lion King Disney GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "text": "Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA" + } + ], + "latest_reactions": [ + { + "message_id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "type": "love", + "score": 1, + "created_at": "2020-01-28T22:17:31.128376Z", + "updated_at": "2020-01-28T22:17:31.128376Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "love": 1 + }, + "reaction_scores": { + "love": 1 + }, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.107978Z", + "updated_at": "2020-01-28T22:17:31.130506Z", + "mentioned_users": [] + }, + { + "id": "16e19c46-fb96-4f89-8031-d5bd2ce3bd64", + "text": "Few can name a topfull mother that isn't a breezeless damage.", + "html": "\u003cp\u003eFew can name a topfull mother that isn’t a breezeless damage.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.153518Z", + "updated_at": "2020-01-28T22:17:31.153518Z", + "mentioned_users": [] + }, + { + "id": "38b1b252-c9a6-4aea-a39e-8de3b7f7604b", + "text": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "Moustache Thumbs Up GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "text": "Discover \u0026 share this Pouce Leve GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "thumb_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "asset_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.155428Z", + "updated_at": "2020-01-28T22:17:31.155428Z", + "mentioned_users": [] + }, + { + "id": "5c7d0c68-72d5-4027-b6b7-711b342d0fc4", + "text": "The carbons could be said to resemble smartish hoods.", + "html": "\u003cp\u003eThe carbons could be said to resemble smartish hoods.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.157811Z", + "updated_at": "2020-01-28T22:17:31.157811Z", + "mentioned_users": [] + }, + { + "id": "5b02535a-0c3c-45fa-9cf8-16f840c5123f", + "text": "Their software was, in this moment, a prolix feature.", + "html": "\u003cp\u003eTheir software was, in this moment, a prolix feature.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.158391Z", + "updated_at": "2020-01-28T22:17:31.158391Z", + "mentioned_users": [] + } + ], + "watcher_count": 1, + "read": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "last_read": "2020-01-28T22:17:31.016937728Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "last_read": "2020-01-28T22:17:31.018856448Z" + } + ], + "members": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "role": "member", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "role": "owner", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + } + ] + }, + { + "channel": { + "id": "spring-voice-7", + "type": "messaging", + "cid": "messaging:spring-voice-7", + "last_message_at": "2020-01-28T22:17:31.194334Z", + "created_at": "2020-01-28T22:17:30.858437Z", + "updated_at": "2020-01-28T22:17:30.858438Z", + "created_by": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "frozen": false, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + } + }, + "messages": [ + { + "id": "9db3ef01-e779-4279-8c54-ffd021eccec4", + "text": "A lustred seal is an alto of the mind.", + "html": "\u003cp\u003eA lustred seal is an alto of the mind.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.167579Z", + "updated_at": "2020-01-28T22:17:31.167579Z", + "mentioned_users": [] + }, + { + "id": "3232e92f-a96f-4b5e-bacb-3565e7155dc4", + "text": "https://giphy.com/gifs/netflix-marvel-the-punisher-l3mZsRS7ZfftbdLdS", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/netflix-marvel-the-punisher-l3mZsRS7ZfftbdLdS\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/netflix-marvel-the-punisher-l3mZsRS7ZfftbdLdS\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "The Punisher Marvel GIF by NETFLIX - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/l3mZsRS7ZfftbdLdS/giphy.gif", + "text": "See What's Next in entertainment and Netflix original series, movies, TV, docs, and comedies. You can stream Netflix anytime, anywhere, on any device.", + "image_url": "https://media.giphy.com/media/l3mZsRS7ZfftbdLdS/giphy.gif", + "thumb_url": "https://media.giphy.com/media/l3mZsRS7ZfftbdLdS/giphy.gif", + "asset_url": "https://media.giphy.com/media/l3mZsRS7ZfftbdLdS/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/netflix-marvel-the-punisher-l3mZsRS7ZfftbdLdS" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.168454Z", + "updated_at": "2020-01-28T22:17:31.168454Z", + "mentioned_users": [] + }, + { + "id": "ae6e196c-0a66-4941-9832-d66a40c95699", + "text": "A height of the parallelogram is assumed to be a sunfast cone.", + "html": "\u003cp\u003eA height of the parallelogram is assumed to be a sunfast cone.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.17494Z", + "updated_at": "2020-01-28T22:17:31.17494Z", + "mentioned_users": [] + }, + { + "id": "38fee958-1efe-456b-b2d2-7bd1ba975eab", + "text": "In modern times the braided bridge reveals itself as a daisied burn to those who look.", + "html": "\u003cp\u003eIn modern times the braided bridge reveals itself as a daisied burn to those who look.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [], + "latest_reactions": [ + { + "message_id": "38fee958-1efe-456b-b2d2-7bd1ba975eab", + "user_id": "spring-voice-7", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "type": "like", + "score": 1, + "created_at": "2020-01-28T22:17:31.213167Z", + "updated_at": "2020-01-28T22:17:31.213167Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "like": 1 + }, + "reaction_scores": { + "like": 1 + }, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.184974Z", + "updated_at": "2020-01-28T22:17:31.215223Z", + "mentioned_users": [] + }, + { + "id": "8dbc9bec-35c4-4c42-942a-5c97bd4400c0", + "text": "Nowhere is it disputed that some posit the unfiled japan to be less than fretted.", + "html": "\u003cp\u003eNowhere is it disputed that some posit the unfiled japan to be less than fretted.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [], + "latest_reactions": [ + { + "message_id": "8dbc9bec-35c4-4c42-942a-5c97bd4400c0", + "user_id": "spring-voice-7", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "type": "love", + "score": 1, + "created_at": "2020-01-28T22:17:31.20517Z", + "updated_at": "2020-01-28T22:17:31.20517Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "love": 1 + }, + "reaction_scores": { + "love": 1 + }, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.18983Z", + "updated_at": "2020-01-28T22:17:31.207426Z", + "mentioned_users": [] + } + ], + "watcher_count": 1, + "members": [] + }, + { + "channel": { + "id": "!members-1xFd7ZzcZ3c3ypDAOvvHynN4Yqy1TV5vw08XTxa4usg", + "type": "messaging", + "cid": "messaging:!members-1xFd7ZzcZ3c3ypDAOvvHynN4Yqy1TV5vw08XTxa4usg", + "last_message_at": "2020-01-28T22:17:31.179366Z", + "created_at": "2020-01-28T22:17:30.951407Z", + "updated_at": "2020-01-28T22:17:30.951407Z", + "created_by": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "frozen": false, + "member_count": 2, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "Robin Papa", + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg" + }, + "messages": [ + { + "id": "c6076f11-7768-4a04-bdf2-c43dddd6d666", + "text": "What we don't know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.", + "html": "\u003cp\u003eWhat we don’t know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.092262Z", + "updated_at": "2020-01-28T22:17:31.092262Z", + "mentioned_users": [] + }, + { + "id": "0fe600b6-9cb7-4d1b-a9ba-eef7dd133b26", + "text": "A plagal ease without britishes is truly a james of premorse entrances.", + "html": "\u003cp\u003eA plagal ease without britishes is truly a james of premorse entrances.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [], + "latest_reactions": [ + { + "message_id": "0fe600b6-9cb7-4d1b-a9ba-eef7dd133b26", + "user_id": "spring-voice-7", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "type": "wow", + "score": 1, + "created_at": "2020-01-28T22:17:31.118131Z", + "updated_at": "2020-01-28T22:17:31.118131Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "wow": 1 + }, + "reaction_scores": { + "wow": 1 + }, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.1022Z", + "updated_at": "2020-01-28T22:17:31.123766Z", + "mentioned_users": [] + }, + { + "id": "4905cb95-c4db-42df-ac6c-5d6531f5f67b", + "text": "A lustred seal is an alto of the mind.", + "html": "\u003cp\u003eA lustred seal is an alto of the mind.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [], + "latest_reactions": [ + { + "message_id": "4905cb95-c4db-42df-ac6c-5d6531f5f67b", + "user_id": "spring-voice-7", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "type": "love", + "score": 1, + "created_at": "2020-01-28T22:17:31.135966Z", + "updated_at": "2020-01-28T22:17:31.135966Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "love": 1 + }, + "reaction_scores": { + "love": 1 + }, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.103279Z", + "updated_at": "2020-01-28T22:17:31.142756Z", + "mentioned_users": [] + }, + { + "id": "549888c4-0d13-48ca-aaa5-7da8effb9c6f", + "text": "Before aftershaves, snowflakes were only deer.", + "html": "\u003cp\u003eBefore aftershaves, snowflakes were only deer.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.10644Z", + "updated_at": "2020-01-28T22:17:31.10644Z", + "mentioned_users": [] + }, + { + "id": "c74784e7-07ef-4b41-a8e3-b2b0e0b6b7ce", + "text": "https://unsplash.com/photos/JdGtvgzQmgQ", + "html": "\u003cp\u003e\u003ca href=\"https://unsplash.com/photos/JdGtvgzQmgQ\" rel=\"nofollow\"\u003ehttps://unsplash.com/photos/JdGtvgzQmgQ\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [ + { + "type": "image", + "title": "Photo by Markus Spiske on Unsplash", + "title_link": "https://unsplash.com/photos/JdGtvgzQmgQ", + "text": "THIS IS NOT A BOT – Save Your Internet – Demo against Uploadfilter – Article 13 #CensorshipMachine – March 16. 2019, Nürnberg, Germany. Download this photo by Markus Spiske on Unsplash", + "image_url": "https://images.unsplash.com/photo-1554272014-73b77edeb47f?ixlib=rb-1.2.1\u0026q=80\u0026fm=jpg\u0026crop=entropy\u0026cs=tinysrgb\u0026w=1080\u0026fit=max\u0026ixid=eyJhcHBfaWQiOjEyMDd9", + "thumb_url": "https://images.unsplash.com/photo-1554272014-73b77edeb47f?ixlib=rb-1.2.1\u0026q=80\u0026fm=jpg\u0026crop=entropy\u0026cs=tinysrgb\u0026w=1080\u0026fit=max\u0026ixid=eyJhcHBfaWQiOjEyMDd9", + "og_scrape_url": "https://unsplash.com/photos/JdGtvgzQmgQ" + } + ], + "latest_reactions": [ + { + "message_id": "c74784e7-07ef-4b41-a8e3-b2b0e0b6b7ce", + "user_id": "spring-voice-7", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "name": "Spring voice", + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice" + }, + "type": "sad", + "score": 1, + "created_at": "2020-01-28T22:17:31.131489Z", + "updated_at": "2020-01-28T22:17:31.131489Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "sad": 1 + }, + "reaction_scores": { + "sad": 1 + }, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.11065Z", + "updated_at": "2020-01-28T22:17:31.133783Z", + "mentioned_users": [] + } + ], + "watcher_count": 1, + "read": [ + { + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "last_read": "2020-01-28T22:17:30.966485504Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "last_read": "2020-01-28T22:17:30.968339456Z" + } + ], + "members": [ + { + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "name": "Robin Papa", + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg" + }, + "role": "member", + "created_at": "2020-01-28T22:17:30.95443Z", + "updated_at": "2020-01-28T22:17:30.95443Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "role": "owner", + "created_at": "2020-01-28T22:17:30.95443Z", + "updated_at": "2020-01-28T22:17:30.95443Z" + } + ] + }, + { + "channel": { + "id": "!members-6ngG6o5HlqKK7B_YIWnamzM4O9IqkfuaFOLO0QVPP-0", + "type": "messaging", + "cid": "messaging:!members-6ngG6o5HlqKK7B_YIWnamzM4O9IqkfuaFOLO0QVPP-0", + "last_message_at": "2020-01-28T22:17:31.171308Z", + "created_at": "2020-01-28T22:17:30.891989Z", + "updated_at": "2020-01-28T22:17:30.891989Z", + "created_by": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "frozen": false, + "member_count": 5, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "image": "https://getstream.imgix.net/images/rn-chat-tutorial/caterpillar_01.png", + "name": "Family" + }, + "messages": [ + { + "id": "0bee6d7f-f479-47bb-998c-f449f6f30d74", + "text": "A described discovery's great-grandfather comes with it the thought that the topmost meter is a tsunami.", + "html": "\u003cp\u003eA described discovery’s great-grandfather comes with it the thought that the topmost meter is a tsunami.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "b816b921-ed3a-4d32-8e9e-f6eca413a7b0", + "role": "user", + "created_at": "2020-01-28T22:17:30.8148Z", + "updated_at": "2020-01-28T22:17:31.08123Z", + "banned": false, + "online": false, + "name": "Micheal Murphy", + "image": "https://randomuser.me/api/portraits/men/95.jpg" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.068584Z", + "updated_at": "2020-01-28T22:17:31.068584Z", + "mentioned_users": [] + }, + { + "id": "76cd8c82-b557-4e48-9d12-87995d3a0e04", + "text": "The biggest driver reveals itself as a sclerosed mom to those who look.", + "html": "\u003cp\u003eThe biggest driver reveals itself as a sclerosed mom to those who look.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "2de0297c-f3f2-489d-b930-ef77342edccf", + "role": "user", + "created_at": "2020-01-28T22:17:30.810011Z", + "updated_at": "2020-01-28T22:17:31.077195Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/45.jpg", + "name": "Daisy Morgan" + }, + "attachments": [], + "latest_reactions": [ + { + "message_id": "76cd8c82-b557-4e48-9d12-87995d3a0e04", + "user_id": "2de0297c-f3f2-489d-b930-ef77342edccf", + "user": { + "id": "2de0297c-f3f2-489d-b930-ef77342edccf", + "role": "user", + "created_at": "2020-01-28T22:17:30.810011Z", + "updated_at": "2020-01-28T22:17:31.077195Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/45.jpg", + "name": "Daisy Morgan" + }, + "type": "wow", + "score": 1, + "created_at": "2020-01-28T22:17:31.108742Z", + "updated_at": "2020-01-28T22:17:31.108742Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "wow": 1 + }, + "reaction_scores": { + "wow": 1 + }, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.077583Z", + "updated_at": "2020-01-28T22:17:31.111709Z", + "mentioned_users": [] + }, + { + "id": "358b8ab3-dd34-4baf-939e-f03617742486", + "text": "A spongy jail without bengals is truly a hawk of rattish canvases.", + "html": "\u003cp\u003eA spongy jail without bengals is truly a hawk of rattish canvases.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "b816b921-ed3a-4d32-8e9e-f6eca413a7b0", + "role": "user", + "created_at": "2020-01-28T22:17:30.8148Z", + "updated_at": "2020-01-28T22:17:31.08123Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/men/95.jpg", + "name": "Micheal Murphy" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.08593Z", + "updated_at": "2020-01-28T22:17:31.08593Z", + "mentioned_users": [] + }, + { + "id": "5a559749-2f45-476e-9610-8eae92f3a5c6", + "text": "https://unsplash.com/photos/4v7ubW7jz1Q", + "html": "\u003cp\u003e\u003ca href=\"https://unsplash.com/photos/4v7ubW7jz1Q\" rel=\"nofollow\"\u003ehttps://unsplash.com/photos/4v7ubW7jz1Q\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "2de0297c-f3f2-489d-b930-ef77342edccf", + "role": "user", + "created_at": "2020-01-28T22:17:30.810011Z", + "updated_at": "2020-01-28T22:17:31.077195Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/45.jpg", + "name": "Daisy Morgan" + }, + "attachments": [ + { + "type": "image", + "title": "Photo by Joel Filipe on Unsplash", + "title_link": "https://unsplash.com/photos/4v7ubW7jz1Q", + "text": "Download this photo by Joel Filipe on Unsplash", + "image_url": "https://images.unsplash.com/photo-1557389352-e721da78ad9f?ixlib=rb-1.2.1\u0026q=80\u0026fm=jpg\u0026crop=entropy\u0026cs=tinysrgb\u0026w=1080\u0026fit=max\u0026ixid=eyJhcHBfaWQiOjEyMDd9", + "thumb_url": "https://images.unsplash.com/photo-1557389352-e721da78ad9f?ixlib=rb-1.2.1\u0026q=80\u0026fm=jpg\u0026crop=entropy\u0026cs=tinysrgb\u0026w=1080\u0026fit=max\u0026ixid=eyJhcHBfaWQiOjEyMDd9", + "og_scrape_url": "https://unsplash.com/photos/4v7ubW7jz1Q" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.090006Z", + "updated_at": "2020-01-28T22:17:31.090006Z", + "mentioned_users": [] + }, + { + "id": "da57c33b-ae12-438c-a672-4e5bad0d1467", + "text": "https://unsplash.com/photos/XttWKETqCCQ", + "html": "\u003cp\u003e\u003ca href=\"https://unsplash.com/photos/XttWKETqCCQ\" rel=\"nofollow\"\u003ehttps://unsplash.com/photos/XttWKETqCCQ\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "6e60091e-2173-4aa8-a6d2-1a79e4cad790", + "role": "user", + "created_at": "2020-01-28T22:17:30.818618Z", + "updated_at": "2020-01-28T22:17:31.056057Z", + "banned": false, + "online": false, + "image": "https://images.unsplash.com/photo-1502378735452-bc7d86632805?ixlib=rb-0.3.5\u0026q=80\u0026fm=jpg\u0026crop=entropy\u0026cs=tinysrgb\u0026w=200\u0026fit=max\u0026s=aa3a807e1bbdfd4364d1f449eaa96d82", + "name": "Carys Metz" + }, + "attachments": [ + { + "type": "image", + "title": "Photo by Olena Sergienko on Unsplash", + "title_link": "https://unsplash.com/photos/XttWKETqCCQ", + "text": "Download this photo by Olena Sergienko on Unsplash", + "image_url": "https://images.unsplash.com/photo-1557053910-d9eadeed1c58?ixlib=rb-1.2.1\u0026q=80\u0026fm=jpg\u0026crop=entropy\u0026cs=tinysrgb\u0026w=1080\u0026fit=max\u0026ixid=eyJhcHBfaWQiOjEyMDd9", + "thumb_url": "https://images.unsplash.com/photo-1557053910-d9eadeed1c58?ixlib=rb-1.2.1\u0026q=80\u0026fm=jpg\u0026crop=entropy\u0026cs=tinysrgb\u0026w=1080\u0026fit=max\u0026ixid=eyJhcHBfaWQiOjEyMDd9", + "og_scrape_url": "https://unsplash.com/photos/XttWKETqCCQ" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.171308Z", + "updated_at": "2020-01-28T22:17:31.171308Z", + "mentioned_users": [] + } + ], + "watcher_count": 1, + "read": [ + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "name": "Spring voice", + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice" + }, + "last_read": "2020-01-28T22:17:30.916570624Z" + }, + { + "user": { + "id": "2de0297c-f3f2-489d-b930-ef77342edccf", + "role": "user", + "created_at": "2020-01-28T22:17:30.810011Z", + "updated_at": "2020-01-28T22:17:31.077195Z", + "banned": false, + "online": false, + "name": "Daisy Morgan", + "image": "https://randomuser.me/api/portraits/women/45.jpg" + }, + "last_read": "2020-01-28T22:17:30.90931712Z" + }, + { + "user": { + "id": "b816b921-ed3a-4d32-8e9e-f6eca413a7b0", + "role": "user", + "created_at": "2020-01-28T22:17:30.8148Z", + "updated_at": "2020-01-28T22:17:31.08123Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/men/95.jpg", + "name": "Micheal Murphy" + }, + "last_read": "2020-01-28T22:17:30.911188736Z" + }, + { + "user": { + "id": "6e60091e-2173-4aa8-a6d2-1a79e4cad790", + "role": "user", + "created_at": "2020-01-28T22:17:30.818618Z", + "updated_at": "2020-01-28T22:17:31.056057Z", + "banned": false, + "online": false, + "image": "https://images.unsplash.com/photo-1502378735452-bc7d86632805?ixlib=rb-0.3.5\u0026q=80\u0026fm=jpg\u0026crop=entropy\u0026cs=tinysrgb\u0026w=200\u0026fit=max\u0026s=aa3a807e1bbdfd4364d1f449eaa96d82", + "name": "Carys Metz" + }, + "last_read": "2020-01-28T22:17:30.912969472Z" + }, + { + "user": { + "id": "dbcd6837-5d93-4b6e-ab27-ee13a490b873", + "role": "user", + "created_at": "2020-01-28T22:17:30.822448Z", + "updated_at": "2020-01-28T22:17:30.823463Z", + "banned": false, + "online": false, + "image": "https://images-na.ssl-images-amazon.com/images/M/MV5BMjAwNjM3NjY5MF5BMl5BanBnXkFtZTcwMjM4NTYwOQ@@._V1_UY256_CR0,0,172,256_AL_.jpg", + "name": "Dakota Fanning" + }, + "last_read": "2020-01-28T22:17:30.914806272Z" + } + ], + "members": [ + { + "user": { + "id": "2de0297c-f3f2-489d-b930-ef77342edccf", + "role": "user", + "created_at": "2020-01-28T22:17:30.810011Z", + "updated_at": "2020-01-28T22:17:31.077195Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/45.jpg", + "name": "Daisy Morgan" + }, + "role": "member", + "created_at": "2020-01-28T22:17:30.895522Z", + "updated_at": "2020-01-28T22:17:30.895522Z" + }, + { + "user": { + "id": "6e60091e-2173-4aa8-a6d2-1a79e4cad790", + "role": "user", + "created_at": "2020-01-28T22:17:30.818618Z", + "updated_at": "2020-01-28T22:17:31.056057Z", + "banned": false, + "online": false, + "image": "https://images.unsplash.com/photo-1502378735452-bc7d86632805?ixlib=rb-0.3.5\u0026q=80\u0026fm=jpg\u0026crop=entropy\u0026cs=tinysrgb\u0026w=200\u0026fit=max\u0026s=aa3a807e1bbdfd4364d1f449eaa96d82", + "name": "Carys Metz" + }, + "role": "member", + "created_at": "2020-01-28T22:17:30.895522Z", + "updated_at": "2020-01-28T22:17:30.895522Z" + }, + { + "user": { + "id": "b816b921-ed3a-4d32-8e9e-f6eca413a7b0", + "role": "user", + "created_at": "2020-01-28T22:17:30.8148Z", + "updated_at": "2020-01-28T22:17:31.08123Z", + "banned": false, + "online": false, + "name": "Micheal Murphy", + "image": "https://randomuser.me/api/portraits/men/95.jpg" + }, + "role": "member", + "created_at": "2020-01-28T22:17:30.895522Z", + "updated_at": "2020-01-28T22:17:30.895522Z" + }, + { + "user": { + "id": "dbcd6837-5d93-4b6e-ab27-ee13a490b873", + "role": "user", + "created_at": "2020-01-28T22:17:30.822448Z", + "updated_at": "2020-01-28T22:17:30.823463Z", + "banned": false, + "online": false, + "name": "Dakota Fanning", + "image": "https://images-na.ssl-images-amazon.com/images/M/MV5BMjAwNjM3NjY5MF5BMl5BanBnXkFtZTcwMjM4NTYwOQ@@._V1_UY256_CR0,0,172,256_AL_.jpg" + }, + "role": "member", + "created_at": "2020-01-28T22:17:30.895523Z", + "updated_at": "2020-01-28T22:17:30.895523Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "role": "owner", + "created_at": "2020-01-28T22:17:30.895523Z", + "updated_at": "2020-01-28T22:17:30.895523Z" + } + ] + }, + { + "channel": { + "id": "still-union-5", + "type": "messaging", + "cid": "messaging:still-union-5", + "last_message_at": "2020-01-24T14:46:19.683016Z", + "created_at": "2020-01-24T14:46:18.808933Z", + "updated_at": "2020-01-24T14:46:18.808933Z", + "created_by": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "frozen": false, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + } + }, + "messages": [ + { + "id": "f77e582d-c4b3-461c-89de-9612a3bee8b1", + "text": "Some posit the phylloid cycle to be less than slimmer.", + "html": "\u003cp\u003eSome posit the phylloid cycle to be less than slimmer.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "attachments": [], + "latest_reactions": [ + { + "message_id": "f77e582d-c4b3-461c-89de-9612a3bee8b1", + "user_id": "still-union-5", + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "type": "angry", + "score": 1, + "created_at": "2020-01-24T14:46:19.14806Z", + "updated_at": "2020-01-24T14:46:19.14806Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "angry": 1 + }, + "reaction_scores": { + "angry": 1 + }, + "reply_count": 0, + "created_at": "2020-01-24T14:46:19.127738Z", + "updated_at": "2020-01-24T14:46:19.149795Z", + "mentioned_users": [] + }, + { + "id": "82e2fbe7-4367-49e3-b80f-fc66e31e5afb", + "text": "A smarty panda is a cactus of the mind.", + "html": "\u003cp\u003eA smarty panda is a cactus of the mind.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-24T14:46:19.165153Z", + "updated_at": "2020-01-24T14:46:19.165154Z", + "mentioned_users": [] + }, + { + "id": "5a5ec35e-c925-4244-9d4c-098b32dd7fce", + "text": "https://unsplash.com/photos/4v7ubW7jz1Q", + "html": "\u003cp\u003e\u003ca href=\"https://unsplash.com/photos/4v7ubW7jz1Q\" rel=\"nofollow\"\u003ehttps://unsplash.com/photos/4v7ubW7jz1Q\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "attachments": [ + { + "type": "image", + "title": "Photo by Joel Filipe on Unsplash", + "title_link": "https://unsplash.com/photos/4v7ubW7jz1Q", + "text": "Download this photo by Joel Filipe on Unsplash", + "image_url": "https://images.unsplash.com/photo-1557389352-e721da78ad9f?ixlib=rb-1.2.1\u0026q=80\u0026fm=jpg\u0026crop=entropy\u0026cs=tinysrgb\u0026w=1080\u0026fit=max\u0026ixid=eyJhcHBfaWQiOjEyMDd9", + "thumb_url": "https://images.unsplash.com/photo-1557389352-e721da78ad9f?ixlib=rb-1.2.1\u0026q=80\u0026fm=jpg\u0026crop=entropy\u0026cs=tinysrgb\u0026w=1080\u0026fit=max\u0026ixid=eyJhcHBfaWQiOjEyMDd9", + "og_scrape_url": "https://unsplash.com/photos/4v7ubW7jz1Q" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-24T14:46:19.168615Z", + "updated_at": "2020-01-24T14:46:19.168615Z", + "mentioned_users": [] + }, + { + "id": "bf27a1a7-911f-4432-8fe4-1d77a7736ef1", + "text": "A volumed ease's eyelash comes with it the thought that the unwiped energy is a flute.", + "html": "\u003cp\u003eA volumed ease’s eyelash comes with it the thought that the unwiped energy is a flute.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "name": "Still union", + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-24T14:46:19.171548Z", + "updated_at": "2020-01-24T14:46:19.171548Z", + "mentioned_users": [] + }, + { + "id": "9cecc024-a5fc-469f-a3a7-69f8bbeda40b", + "text": "https://www.youtube.com/watch?v=sCtixpIWBto", + "html": "\u003cp\u003e\u003ca href=\"https://www.youtube.com/watch?v=sCtixpIWBto\" rel=\"nofollow\"\u003ehttps://www.youtube.com/watch?v=sCtixpIWBto\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "name": "Still union", + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union" + }, + "attachments": [ + { + "type": "video", + "author_name": "YouTube", + "title": "Rachmaninoff - Prelude in C Sharp Minor (Op. 3 No. 2)", + "title_link": "https://www.youtube.com/watch?v=sCtixpIWBto", + "text": "Rachmaninoff - Prelude in C Sharp Minor (Op. 3 No. 2) Click the 🔔bell to always be notified on new uploads! ♫ Listen on Spotify: http://spoti.fi/2LdpqK7 ♫ MI...", + "image_url": "https://i.ytimg.com/vi/sCtixpIWBto/maxresdefault.jpg", + "thumb_url": "https://i.ytimg.com/vi/sCtixpIWBto/maxresdefault.jpg", + "asset_url": "https://www.youtube.com/embed/sCtixpIWBto", + "og_scrape_url": "https://www.youtube.com/watch?v=sCtixpIWBto" + } + ], + "latest_reactions": [ + { + "message_id": "9cecc024-a5fc-469f-a3a7-69f8bbeda40b", + "user_id": "still-union-5", + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "type": "like", + "score": 1, + "created_at": "2020-01-24T14:46:19.703554Z", + "updated_at": "2020-01-24T14:46:19.703554Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "like": 1 + }, + "reaction_scores": { + "like": 1 + }, + "reply_count": 0, + "created_at": "2020-01-24T14:46:19.683016Z", + "updated_at": "2020-01-24T14:46:19.705034Z", + "mentioned_users": [] + } + ], + "watcher_count": 1, + "members": [] + }, + { + "channel": { + "id": "!members-9CAnYw3rnRNrqnSyxvA1rQyL6HLVTt1KUgAz1CeoEvo", + "type": "messaging", + "cid": "messaging:!members-9CAnYw3rnRNrqnSyxvA1rQyL6HLVTt1KUgAz1CeoEvo", + "last_message_at": "2020-01-24T14:46:19.168507Z", + "created_at": "2020-01-24T14:46:18.846673Z", + "updated_at": "2020-01-24T14:46:18.846673Z", + "created_by": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "frozen": false, + "member_count": 5, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "image": "https://getstream.imgix.net/images/rn-chat-tutorial/caterpillar_01.png", + "name": "Family" + }, + "messages": [ + { + "id": "a5ee396a-34bf-42d9-8388-819e84d770e2", + "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "7cdf7f97-c336-4263-9430-42a1f4d165c8", + "role": "user", + "created_at": "2020-01-24T14:46:18.764869Z", + "updated_at": "2020-01-24T14:46:19.007916Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "The Lion King Disney GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "text": "Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-24T14:46:19.014504Z", + "updated_at": "2020-01-24T14:46:19.014505Z", + "mentioned_users": [] + }, + { + "id": "31bdba04-b86f-48d6-91d9-972bb50e3ffe", + "text": "A chiefly care is a pressure of the mind.", + "html": "\u003cp\u003eA chiefly care is a pressure of the mind.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "5a46018e-252b-4598-a7e8-40949c3be4d6", + "role": "user", + "created_at": "2020-01-24T14:46:18.783718Z", + "updated_at": "2020-01-24T14:46:19.096044Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/44.jpg", + "name": "June Cha" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-24T14:46:19.050641Z", + "updated_at": "2020-01-24T14:46:19.050641Z", + "mentioned_users": [] + }, + { + "id": "bc08ad2e-4da0-4d9f-b349-4e9b85d45899", + "text": "What we don't know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.", + "html": "\u003cp\u003eWhat we don’t know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "694bc286-80d5-4be5-81f8-068028d561e4", + "role": "user", + "created_at": "2020-01-24T14:46:18.760346Z", + "updated_at": "2020-01-24T14:46:19.132067Z", + "banned": false, + "online": false, + "name": "Adelle Charles", + "image": "https://pbs.twimg.com/profile_images/1108790938640531456/Bl2JvdG_.png" + }, + "attachments": [], + "latest_reactions": [ + { + "message_id": "bc08ad2e-4da0-4d9f-b349-4e9b85d45899", + "user_id": "694bc286-80d5-4be5-81f8-068028d561e4", + "user": { + "id": "694bc286-80d5-4be5-81f8-068028d561e4", + "role": "user", + "created_at": "2020-01-24T14:46:18.760346Z", + "updated_at": "2020-01-24T14:46:19.132067Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/1108790938640531456/Bl2JvdG_.png", + "name": "Adelle Charles" + }, + "type": "angry", + "score": 1, + "created_at": "2020-01-24T14:46:19.08354Z", + "updated_at": "2020-01-24T14:46:19.08354Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "angry": 1 + }, + "reaction_scores": { + "angry": 1 + }, + "reply_count": 1, + "created_at": "2020-01-24T14:46:19.055407Z", + "updated_at": "2020-01-24T14:46:19.08681Z", + "mentioned_users": [] + }, + { + "id": "9b0063ad-cd62-4dc7-ba21-3feee5853122", + "text": "A height of the parallelogram is assumed to be a sunfast cone.", + "html": "\u003cp\u003eA height of the parallelogram is assumed to be a sunfast cone.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "attachments": [], + "latest_reactions": [ + { + "message_id": "9b0063ad-cd62-4dc7-ba21-3feee5853122", + "user_id": "still-union-5", + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "type": "love", + "score": 1, + "created_at": "2020-01-24T14:46:19.094595Z", + "updated_at": "2020-01-24T14:46:19.094595Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "love": 1 + }, + "reaction_scores": { + "love": 1 + }, + "reply_count": 0, + "created_at": "2020-01-24T14:46:19.068177Z", + "updated_at": "2020-01-24T14:46:19.096185Z", + "mentioned_users": [] + }, + { + "id": "4f11cd19-66cc-4fc4-b153-9eced2518e21", + "text": "Few can name a topfull mother that isn't a breezeless damage.", + "html": "\u003cp\u003eFew can name a topfull mother that isn’t a breezeless damage.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "694bc286-80d5-4be5-81f8-068028d561e4", + "role": "user", + "created_at": "2020-01-24T14:46:18.760346Z", + "updated_at": "2020-01-24T14:46:19.132067Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/1108790938640531456/Bl2JvdG_.png", + "name": "Adelle Charles" + }, + "attachments": [], + "latest_reactions": [ + { + "message_id": "4f11cd19-66cc-4fc4-b153-9eced2518e21", + "user_id": "still-union-5", + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "type": "haha", + "score": 1, + "created_at": "2020-01-24T14:46:19.110667Z", + "updated_at": "2020-01-24T14:46:19.110667Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "haha": 1 + }, + "reaction_scores": { + "haha": 1 + }, + "reply_count": 1, + "created_at": "2020-01-24T14:46:19.077713Z", + "updated_at": "2020-01-24T14:46:19.113185Z", + "mentioned_users": [] + } + ], + "watcher_count": 1, + "read": [ + { + "user": { + "id": "0f345a0c-3f35-4e2f-805e-77f53f8332cd", + "role": "user", + "created_at": "2020-01-24T14:46:18.779973Z", + "updated_at": "2020-01-24T14:46:19.157321Z", + "banned": false, + "online": false, + "image": "https://images-na.ssl-images-amazon.com/images/M/MV5BMDc2M2NkMTctNmQ0MS00MjQxLWFkMGItNGY1Y2Y3NzYzZjg1XkEyXkFqcGdeQXVyNjAzMTgxNjY@._V1_UY256_CR74,0,172,256_AL_.jpg", + "name": "Zoe McLellan" + }, + "last_read": "2020-01-24T14:46:18.863312384Z" + }, + { + "user": { + "id": "5a46018e-252b-4598-a7e8-40949c3be4d6", + "role": "user", + "created_at": "2020-01-24T14:46:18.783718Z", + "updated_at": "2020-01-24T14:46:19.096044Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/44.jpg", + "name": "June Cha" + }, + "last_read": "2020-01-24T14:46:18.8644352Z" + }, + { + "user": { + "id": "7cdf7f97-c336-4263-9430-42a1f4d165c8", + "role": "user", + "created_at": "2020-01-24T14:46:18.764869Z", + "updated_at": "2020-01-24T14:46:19.007916Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "last_read": "2020-01-24T14:46:18.866511104Z" + }, + { + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "last_read": "2020-01-24T14:46:18.867566336Z" + }, + { + "user": { + "id": "694bc286-80d5-4be5-81f8-068028d561e4", + "role": "user", + "created_at": "2020-01-24T14:46:18.760346Z", + "updated_at": "2020-01-24T14:46:19.132067Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/1108790938640531456/Bl2JvdG_.png", + "name": "Adelle Charles" + }, + "last_read": "2020-01-24T14:46:18.865476096Z" + } + ], + "members": [ + { + "user": { + "id": "0f345a0c-3f35-4e2f-805e-77f53f8332cd", + "role": "user", + "created_at": "2020-01-24T14:46:18.779973Z", + "updated_at": "2020-01-24T14:46:19.157321Z", + "banned": false, + "online": false, + "image": "https://images-na.ssl-images-amazon.com/images/M/MV5BMDc2M2NkMTctNmQ0MS00MjQxLWFkMGItNGY1Y2Y3NzYzZjg1XkEyXkFqcGdeQXVyNjAzMTgxNjY@._V1_UY256_CR74,0,172,256_AL_.jpg", + "name": "Zoe McLellan" + }, + "role": "member", + "created_at": "2020-01-24T14:46:18.850178Z", + "updated_at": "2020-01-24T14:46:18.850178Z" + }, + { + "user": { + "id": "5a46018e-252b-4598-a7e8-40949c3be4d6", + "role": "user", + "created_at": "2020-01-24T14:46:18.783718Z", + "updated_at": "2020-01-24T14:46:19.096044Z", + "banned": false, + "online": false, + "name": "June Cha", + "image": "https://randomuser.me/api/portraits/women/44.jpg" + }, + "role": "member", + "created_at": "2020-01-24T14:46:18.850179Z", + "updated_at": "2020-01-24T14:46:18.850179Z" + }, + { + "user": { + "id": "694bc286-80d5-4be5-81f8-068028d561e4", + "role": "user", + "created_at": "2020-01-24T14:46:18.760346Z", + "updated_at": "2020-01-24T14:46:19.132067Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/1108790938640531456/Bl2JvdG_.png", + "name": "Adelle Charles" + }, + "role": "member", + "created_at": "2020-01-24T14:46:18.850179Z", + "updated_at": "2020-01-24T14:46:18.850179Z" + }, + { + "user": { + "id": "7cdf7f97-c336-4263-9430-42a1f4d165c8", + "role": "user", + "created_at": "2020-01-24T14:46:18.764869Z", + "updated_at": "2020-01-24T14:46:19.007916Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "role": "member", + "created_at": "2020-01-24T14:46:18.850179Z", + "updated_at": "2020-01-24T14:46:18.850179Z" + }, + { + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "role": "owner", + "created_at": "2020-01-24T14:46:18.85018Z", + "updated_at": "2020-01-24T14:46:18.85018Z" + } + ] + }, + { + "channel": { + "id": "!members-Hc19Pf6huW0QTLRkQscRvline00uxqpj2OAYIcxTPFw", + "type": "messaging", + "cid": "messaging:!members-Hc19Pf6huW0QTLRkQscRvline00uxqpj2OAYIcxTPFw", + "last_message_at": "2020-01-24T14:46:19.16305Z", + "created_at": "2020-01-24T14:46:18.953227Z", + "updated_at": "2020-01-24T14:46:18.953227Z", + "created_by": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "frozen": false, + "member_count": 2, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "image": "https://randomuser.me/api/portraits/women/8.jpg", + "name": "Jennifer Fritz" + }, + "messages": [ + { + "id": "17b0dc4a-78b3-4e08-860e-11ad2d8ac89d", + "text": "Extending this logic, the witted tree reveals itself as a glummest danger to those who look.", + "html": "\u003cp\u003eExtending this logic, the witted tree reveals itself as a glummest danger to those who look.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "attachments": [], + "latest_reactions": [ + { + "message_id": "17b0dc4a-78b3-4e08-860e-11ad2d8ac89d", + "user_id": "05ab6a02-9160-47b7-87d9-c6888fa85f83", + "user": { + "id": "05ab6a02-9160-47b7-87d9-c6888fa85f83", + "role": "user", + "created_at": "2020-01-24T14:46:18.772459Z", + "updated_at": "2020-01-24T14:46:19.151717Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/8.jpg", + "name": "Jennifer Fritz" + }, + "type": "like", + "score": 1, + "created_at": "2020-01-24T14:46:19.08279Z", + "updated_at": "2020-01-24T14:46:19.08279Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "like": 1 + }, + "reaction_scores": { + "like": 1 + }, + "reply_count": 0, + "created_at": "2020-01-24T14:46:19.051688Z", + "updated_at": "2020-01-24T14:46:19.085197Z", + "mentioned_users": [] + }, + { + "id": "ca22e5e3-5f2c-456e-8699-522cba94d177", + "text": "A cricoid melody without replaces is truly a cocktail of unripe badgers.", + "html": "\u003cp\u003eA cricoid melody without replaces is truly a cocktail of unripe badgers.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "05ab6a02-9160-47b7-87d9-c6888fa85f83", + "role": "user", + "created_at": "2020-01-24T14:46:18.772459Z", + "updated_at": "2020-01-24T14:46:19.151717Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/8.jpg", + "name": "Jennifer Fritz" + }, + "attachments": [], + "latest_reactions": [ + { + "message_id": "ca22e5e3-5f2c-456e-8699-522cba94d177", + "user_id": "05ab6a02-9160-47b7-87d9-c6888fa85f83", + "user": { + "id": "05ab6a02-9160-47b7-87d9-c6888fa85f83", + "role": "user", + "created_at": "2020-01-24T14:46:18.772459Z", + "updated_at": "2020-01-24T14:46:19.151717Z", + "banned": false, + "online": false, + "name": "Jennifer Fritz", + "image": "https://randomuser.me/api/portraits/women/8.jpg" + }, + "type": "like", + "score": 1, + "created_at": "2020-01-24T14:46:19.133825Z", + "updated_at": "2020-01-24T14:46:19.133825Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "like": 1 + }, + "reaction_scores": { + "like": 1 + }, + "reply_count": 0, + "created_at": "2020-01-24T14:46:19.10105Z", + "updated_at": "2020-01-24T14:46:19.135877Z", + "mentioned_users": [] + }, + { + "id": "494916be-b66b-48b0-a1c5-bb2cab32eb7a", + "text": "The first towy harmony is, in its own way, a voyage.", + "html": "\u003cp\u003eThe first towy harmony is, in its own way, a voyage.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-24T14:46:19.115607Z", + "updated_at": "2020-01-24T14:46:19.115608Z", + "mentioned_users": [] + }, + { + "id": "633c149b-2b1d-461a-8dfe-0f941acc34d4", + "text": "A bicycle can hardly be considered a yearning jar without also being an alley.", + "html": "\u003cp\u003eA bicycle can hardly be considered a yearning jar without also being an alley.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "05ab6a02-9160-47b7-87d9-c6888fa85f83", + "role": "user", + "created_at": "2020-01-24T14:46:18.772459Z", + "updated_at": "2020-01-24T14:46:19.151717Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/8.jpg", + "name": "Jennifer Fritz" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-24T14:46:19.135028Z", + "updated_at": "2020-01-24T14:46:19.135028Z", + "mentioned_users": [] + }, + { + "id": "2378fa62-8a5f-4dd3-8ea0-e0276e682c8a", + "text": "https://unsplash.com/photos/4v7ubW7jz1Q", + "html": "\u003cp\u003e\u003ca href=\"https://unsplash.com/photos/4v7ubW7jz1Q\" rel=\"nofollow\"\u003ehttps://unsplash.com/photos/4v7ubW7jz1Q\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "05ab6a02-9160-47b7-87d9-c6888fa85f83", + "role": "user", + "created_at": "2020-01-24T14:46:18.772459Z", + "updated_at": "2020-01-24T14:46:19.151717Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/8.jpg", + "name": "Jennifer Fritz" + }, + "attachments": [ + { + "type": "image", + "title": "Photo by Joel Filipe on Unsplash", + "title_link": "https://unsplash.com/photos/4v7ubW7jz1Q", + "text": "Download this photo by Joel Filipe on Unsplash", + "image_url": "https://images.unsplash.com/photo-1557389352-e721da78ad9f?ixlib=rb-1.2.1\u0026q=80\u0026fm=jpg\u0026crop=entropy\u0026cs=tinysrgb\u0026w=1080\u0026fit=max\u0026ixid=eyJhcHBfaWQiOjEyMDd9", + "thumb_url": "https://images.unsplash.com/photo-1557389352-e721da78ad9f?ixlib=rb-1.2.1\u0026q=80\u0026fm=jpg\u0026crop=entropy\u0026cs=tinysrgb\u0026w=1080\u0026fit=max\u0026ixid=eyJhcHBfaWQiOjEyMDd9", + "og_scrape_url": "https://unsplash.com/photos/4v7ubW7jz1Q" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-24T14:46:19.140074Z", + "updated_at": "2020-01-24T14:46:19.140075Z", + "mentioned_users": [] + } + ], + "watcher_count": 1, + "read": [ + { + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "last_read": "2020-01-24T14:46:18.969139712Z" + }, + { + "user": { + "id": "05ab6a02-9160-47b7-87d9-c6888fa85f83", + "role": "user", + "created_at": "2020-01-24T14:46:18.772459Z", + "updated_at": "2020-01-24T14:46:19.151717Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/8.jpg", + "name": "Jennifer Fritz" + }, + "last_read": "2020-01-24T14:46:18.96800512Z" + } + ], + "members": [ + { + "user": { + "id": "05ab6a02-9160-47b7-87d9-c6888fa85f83", + "role": "user", + "created_at": "2020-01-24T14:46:18.772459Z", + "updated_at": "2020-01-24T14:46:19.151717Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/8.jpg", + "name": "Jennifer Fritz" + }, + "role": "member", + "created_at": "2020-01-24T14:46:18.956793Z", + "updated_at": "2020-01-24T14:46:18.956793Z" + }, + { + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "role": "owner", + "created_at": "2020-01-24T14:46:18.956793Z", + "updated_at": "2020-01-24T14:46:18.956793Z" + } + ] + }, + { + "channel": { + "id": "!members-og1pgcoaqCSdh6KyIXz4qKB_d22pnWth6Yfbovd8yP0", + "type": "messaging", + "cid": "messaging:!members-og1pgcoaqCSdh6KyIXz4qKB_d22pnWth6Yfbovd8yP0", + "last_message_at": "2020-01-24T14:46:19.124398Z", + "created_at": "2020-01-24T14:46:18.902415Z", + "updated_at": "2020-01-24T14:46:18.902415Z", + "created_by": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "frozen": false, + "member_count": 2, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "Ana De Armas", + "image": "https://images-na.ssl-images-amazon.com/images/M/MV5BMjA3NjYzMzE1MV5BMl5BanBnXkFtZTgwNTA4NDY4OTE@._V1_UX172_CR0,0,172,256_AL_.jpg" + }, + "messages": [ + { + "id": "1aec347f-c1c4-4d83-ab42-76a12d3d3da2", + "text": "Before aftershaves, snowflakes were only deer.", + "html": "\u003cp\u003eBefore aftershaves, snowflakes were only deer.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-24T14:46:19.057578Z", + "updated_at": "2020-01-24T14:46:19.057578Z", + "mentioned_users": [] + }, + { + "id": "1ec1473d-405d-4a3c-b771-ab09fcbe071f", + "text": "https://giphy.com/gifs/movie-trailer-gemini-man-QsU9X0AxxfSAwsaz7n", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/movie-trailer-gemini-man-QsU9X0AxxfSAwsaz7n\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/movie-trailer-gemini-man-QsU9X0AxxfSAwsaz7n\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "Gemini Man Trailer GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/QsU9X0AxxfSAwsaz7n/giphy.gif", + "text": "Discover \u0026 share this Gemini Man GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/QsU9X0AxxfSAwsaz7n/giphy.gif", + "thumb_url": "https://media.giphy.com/media/QsU9X0AxxfSAwsaz7n/giphy.gif", + "asset_url": "https://media.giphy.com/media/QsU9X0AxxfSAwsaz7n/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/movie-trailer-gemini-man-QsU9X0AxxfSAwsaz7n" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-24T14:46:19.076425Z", + "updated_at": "2020-01-24T14:46:19.076425Z", + "mentioned_users": [] + }, + { + "id": "3c6b5f22-da06-470e-bb7f-499fa7466de0", + "text": "Their chord was, in this moment, a slipshod parenthesis.", + "html": "\u003cp\u003eTheir chord was, in this moment, a slipshod parenthesis.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "db375ac9-6a3f-4d9c-a5ea-4cd6c169fd09", + "role": "user", + "created_at": "2020-01-24T14:46:18.768629Z", + "updated_at": "2020-01-24T14:46:19.076467Z", + "banned": false, + "online": false, + "image": "https://images-na.ssl-images-amazon.com/images/M/MV5BMjA3NjYzMzE1MV5BMl5BanBnXkFtZTgwNTA4NDY4OTE@._V1_UX172_CR0,0,172,256_AL_.jpg", + "name": "Ana De Armas" + }, + "attachments": [], + "latest_reactions": [ + { + "message_id": "3c6b5f22-da06-470e-bb7f-499fa7466de0", + "user_id": "db375ac9-6a3f-4d9c-a5ea-4cd6c169fd09", + "user": { + "id": "db375ac9-6a3f-4d9c-a5ea-4cd6c169fd09", + "role": "user", + "created_at": "2020-01-24T14:46:18.768629Z", + "updated_at": "2020-01-24T14:46:19.076467Z", + "banned": false, + "online": false, + "image": "https://images-na.ssl-images-amazon.com/images/M/MV5BMjA3NjYzMzE1MV5BMl5BanBnXkFtZTgwNTA4NDY4OTE@._V1_UX172_CR0,0,172,256_AL_.jpg", + "name": "Ana De Armas" + }, + "type": "like", + "score": 1, + "created_at": "2020-01-24T14:46:19.118307Z", + "updated_at": "2020-01-24T14:46:19.118307Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "like": 1 + }, + "reaction_scores": { + "like": 1 + }, + "reply_count": 0, + "created_at": "2020-01-24T14:46:19.078556Z", + "updated_at": "2020-01-24T14:46:19.120491Z", + "mentioned_users": [] + }, + { + "id": "86eb957d-54fd-4ed9-8e80-1bb3212e57ea", + "text": "However, the porcine puffin reveals itself as an uncaught mirror to those who look.", + "html": "\u003cp\u003eHowever, the porcine puffin reveals itself as an uncaught mirror to those who look.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-24T14:46:19.078749Z", + "updated_at": "2020-01-24T14:46:19.07875Z", + "mentioned_users": [] + }, + { + "id": "179eb482-b005-4b34-965c-22b1dbb60a8e", + "text": "A parklike screw without ants is truly a fly of hooly rings.", + "html": "\u003cp\u003eA parklike screw without ants is truly a fly of hooly rings.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "db375ac9-6a3f-4d9c-a5ea-4cd6c169fd09", + "role": "user", + "created_at": "2020-01-24T14:46:18.768629Z", + "updated_at": "2020-01-24T14:46:19.076467Z", + "banned": false, + "online": false, + "name": "Ana De Armas", + "image": "https://images-na.ssl-images-amazon.com/images/M/MV5BMjA3NjYzMzE1MV5BMl5BanBnXkFtZTgwNTA4NDY4OTE@._V1_UX172_CR0,0,172,256_AL_.jpg" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-24T14:46:19.080192Z", + "updated_at": "2020-01-24T14:46:19.080192Z", + "mentioned_users": [] + } + ], + "watcher_count": 1, + "read": [ + { + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "last_read": "2020-01-24T14:46:18.92038528Z" + }, + { + "user": { + "id": "db375ac9-6a3f-4d9c-a5ea-4cd6c169fd09", + "role": "user", + "created_at": "2020-01-24T14:46:18.768629Z", + "updated_at": "2020-01-24T14:46:19.076467Z", + "banned": false, + "online": false, + "image": "https://images-na.ssl-images-amazon.com/images/M/MV5BMjA3NjYzMzE1MV5BMl5BanBnXkFtZTgwNTA4NDY4OTE@._V1_UX172_CR0,0,172,256_AL_.jpg", + "name": "Ana De Armas" + }, + "last_read": "2020-01-24T14:46:18.919357952Z" + } + ], + "members": [ + { + "user": { + "id": "db375ac9-6a3f-4d9c-a5ea-4cd6c169fd09", + "role": "user", + "created_at": "2020-01-24T14:46:18.768629Z", + "updated_at": "2020-01-24T14:46:19.076467Z", + "banned": false, + "online": false, + "image": "https://images-na.ssl-images-amazon.com/images/M/MV5BMjA3NjYzMzE1MV5BMl5BanBnXkFtZTgwNTA4NDY4OTE@._V1_UX172_CR0,0,172,256_AL_.jpg", + "name": "Ana De Armas" + }, + "role": "member", + "created_at": "2020-01-24T14:46:18.905968Z", + "updated_at": "2020-01-24T14:46:18.905968Z" + }, + { + "user": { + "id": "still-union-5", + "role": "user", + "created_at": "2020-01-24T14:46:18.776236Z", + "updated_at": "2020-01-24T14:46:32.065913Z", + "last_active": "2020-01-24T14:46:32.05853Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=still-union-5\u0026amp;name=Still+union", + "name": "Still union" + }, + "role": "owner", + "created_at": "2020-01-24T14:46:18.905968Z", + "updated_at": "2020-01-24T14:46:18.905968Z" + } + ] + }, + { + "channel": { + "id": "holy-sun-6", + "type": "messaging", + "cid": "messaging:holy-sun-6", + "last_message_at": "2020-01-23T00:52:40.316069Z", + "created_at": "2020-01-23T00:52:39.457615Z", + "updated_at": "2020-01-23T00:52:39.457615Z", + "created_by": { + "id": "holy-sun-6", + "role": "user", + "created_at": "2020-01-23T00:52:39.412193Z", + "updated_at": "2020-01-23T00:52:40.211607Z", + "banned": false, + "online": false, + "name": "Holy sun", + "image": "https://getstream.io/random_svg/?id=holy-sun-6\u0026name=Holy+sun" + }, + "frozen": false, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + } + }, + "messages": [ + { + "id": "d0843e2b-2dd8-404e-a4a4-3730287d144b", + "text": "A robert of the august is assumed to be an accurst fortnight.", + "html": "\u003cp\u003eA robert of the august is assumed to be an accurst fortnight.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "holy-sun-6", + "role": "user", + "created_at": "2020-01-23T00:52:39.412193Z", + "updated_at": "2020-01-23T00:52:40.211607Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=holy-sun-6\u0026name=Holy+sun", + "name": "Holy sun" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-23T00:52:39.828172Z", + "updated_at": "2020-01-23T00:52:39.828172Z", + "mentioned_users": [] + }, + { + "id": "af6c4391-be20-4b25-b824-b4751507cd33", + "text": "Authors often misinterpret the railway as a louvred range, when in actuality it feels more like a pitted fridge.", + "html": "\u003cp\u003eAuthors often misinterpret the railway as a louvred range, when in actuality it feels more like a pitted fridge.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "holy-sun-6", + "role": "user", + "created_at": "2020-01-23T00:52:39.412193Z", + "updated_at": "2020-01-23T00:52:40.211607Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=holy-sun-6\u0026name=Holy+sun", + "name": "Holy sun" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-23T00:52:39.850576Z", + "updated_at": "2020-01-23T00:52:39.850577Z", + "mentioned_users": [] + }, + { + "id": "960d62ad-00c6-4237-9b3c-cd440408ea18", + "text": "A dugout can hardly be considered a soundless acknowledgment without also being a raft.", + "html": "\u003cp\u003eA dugout can hardly be considered a soundless acknowledgment without also being a raft.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "holy-sun-6", + "role": "user", + "created_at": "2020-01-23T00:52:39.412193Z", + "updated_at": "2020-01-23T00:52:40.211607Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=holy-sun-6\u0026name=Holy+sun", + "name": "Holy sun" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-23T00:52:39.873187Z", + "updated_at": "2020-01-23T00:52:39.873187Z", + "mentioned_users": [] + }, + { + "id": "409074d7-c830-4c16-9205-393de08898d3", + "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "holy-sun-6", + "role": "user", + "created_at": "2020-01-23T00:52:39.412193Z", + "updated_at": "2020-01-23T00:52:40.211607Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=holy-sun-6\u0026name=Holy+sun", + "name": "Holy sun" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "The Lion King Disney GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "text": "Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-23T00:52:40.191037Z", + "updated_at": "2020-01-23T00:52:40.191037Z", + "mentioned_users": [] + }, + { + "id": "2f1b8944-014a-4753-886f-1042163ada46", + "text": "https://www.youtube.com/watch?v=sCtixpIWBto", + "html": "\u003cp\u003e\u003ca href=\"https://www.youtube.com/watch?v=sCtixpIWBto\" rel=\"nofollow\"\u003ehttps://www.youtube.com/watch?v=sCtixpIWBto\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "holy-sun-6", + "role": "user", + "created_at": "2020-01-23T00:52:39.412193Z", + "updated_at": "2020-01-23T00:52:40.211607Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=holy-sun-6\u0026name=Holy+sun", + "name": "Holy sun" + }, + "attachments": [ + { + "type": "video", + "author_name": "YouTube", + "title": "Rachmaninoff - Prelude in C Sharp Minor (Op. 3 No. 2)", + "title_link": "https://www.youtube.com/watch?v=sCtixpIWBto", + "text": "Rachmaninoff - Prelude in C Sharp Minor (Op. 3 No. 2) Click the 🔔bell to always be notified on new uploads! ♫ Listen on Spotify: http://spoti.fi/2LdpqK7 ♫ MI...", + "image_url": "https://i.ytimg.com/vi/sCtixpIWBto/maxresdefault.jpg", + "thumb_url": "https://i.ytimg.com/vi/sCtixpIWBto/maxresdefault.jpg", + "asset_url": "https://www.youtube.com/embed/sCtixpIWBto", + "og_scrape_url": "https://www.youtube.com/watch?v=sCtixpIWBto" + } + ], + "latest_reactions": [ + { + "message_id": "2f1b8944-014a-4753-886f-1042163ada46", + "user_id": "holy-sun-6", + "user": { + "id": "holy-sun-6", + "role": "user", + "created_at": "2020-01-23T00:52:39.412193Z", + "updated_at": "2020-01-23T00:52:40.211607Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=holy-sun-6\u0026name=Holy+sun", + "name": "Holy sun" + }, + "type": "haha", + "score": 1, + "created_at": "2020-01-23T00:52:40.336054Z", + "updated_at": "2020-01-23T00:52:40.336054Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "haha": 1 + }, + "reaction_scores": { + "haha": 1 + }, + "reply_count": 0, + "created_at": "2020-01-23T00:52:40.316069Z", + "updated_at": "2020-01-23T00:52:40.337791Z", + "mentioned_users": [] + } + ], + "watcher_count": 1, + "members": [] + }, + { + "channel": { + "id": "!members-aOjLAUKF7f4tB6iysBURUm7soQp6BBPpr0chg1KjGAA", + "type": "messaging", + "cid": "messaging:!members-aOjLAUKF7f4tB6iysBURUm7soQp6BBPpr0chg1KjGAA", + "last_message_at": "2020-01-23T00:52:40.193868Z", + "created_at": "2020-01-23T00:52:39.556322Z", + "updated_at": "2020-01-23T00:52:39.556322Z", + "created_by": { + "id": "holy-sun-6", + "role": "user", + "created_at": "2020-01-23T00:52:39.412193Z", + "updated_at": "2020-01-23T00:52:40.211607Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=holy-sun-6\u0026name=Holy+sun", + "name": "Holy sun" + }, + "frozen": false, + "member_count": 2, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "image": "https://randomuser.me/api/portraits/women/65.jpg", + "name": "Renee Sims" + }, + "messages": [ + { + "id": "0fd2c220-4871-49d6-873d-84e7e12782e9", + "text": "A smarty panda is a cactus of the mind.", + "html": "\u003cp\u003eA smarty panda is a cactus of the mind.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "636fdc1b-8b86-47b0-8ea1-8bb7f7c38910", + "role": "user", + "created_at": "2020-01-23T00:52:39.43164Z", + "updated_at": "2020-01-23T00:52:39.798407Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/65.jpg", + "name": "Renee Sims" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-23T00:52:39.763764Z", + "updated_at": "2020-01-23T00:52:39.763764Z", + "mentioned_users": [] + }, + { + "id": "85a2e430-f9af-4e60-a43e-c87e48102aa7", + "text": "We can assume that any instance of a half-brother can be construed as an allowed half-brother.", + "html": "\u003cp\u003eWe can assume that any instance of a half-brother can be construed as an allowed half-brother.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "holy-sun-6", + "role": "user", + "created_at": "2020-01-23T00:52:39.412193Z", + "updated_at": "2020-01-23T00:52:40.211607Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=holy-sun-6\u0026name=Holy+sun", + "name": "Holy sun" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-23T00:52:39.764235Z", + "updated_at": "2020-01-23T00:52:39.764236Z", + "mentioned_users": [] + }, + { + "id": "8bc5aeb6-6b39-488f-95ac-8c4bbb704b0e", + "text": "https://www.sciencenews.org/article/peacock-spiders-superblack-spots-reflect-just-05-percent-light?tgt=nr", + "html": "\u003cp\u003e\u003ca href=\"https://www.sciencenews.org/article/peacock-spiders-superblack-spots-reflect-just-05-percent-light?tgt=nr\" rel=\"nofollow\"\u003ehttps://www.sciencenews.org/article/peacock-spiders-superblack-spots-reflect-just-05-percent-light?tgt=nr\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "holy-sun-6", + "role": "user", + "created_at": "2020-01-23T00:52:39.412193Z", + "updated_at": "2020-01-23T00:52:40.211607Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=holy-sun-6\u0026name=Holy+sun", + "name": "Holy sun" + }, + "attachments": [ + { + "type": "image", + "author_name": "Science News", + "title": "Peacock spiders’ superblack spots reflect just 0.5 percent of light", + "title_link": "https://www.sciencenews.org/article/peacock-spiders-superblack-spots-reflect-just-05-percent-light", + "text": "By manipulating light with tiny structures, patches on peacock spiders appear superblack, helping accentuate the arachnids’ bright colors.", + "image_url": "https://www.sciencenews.org/wp-content/uploads/2019/05/051419_cw_spider_feat.jpg", + "thumb_url": "https://www.sciencenews.org/wp-content/uploads/2019/05/051419_cw_spider_feat.jpg", + "og_scrape_url": "https://www.sciencenews.org/article/peacock-spiders-superblack-spots-reflect-just-05-percent-light?tgt=nr" + } + ], + "latest_reactions": [ + { + "message_id": "8bc5aeb6-6b39-488f-95ac-8c4bbb704b0e", + "user_id": "636fdc1b-8b86-47b0-8ea1-8bb7f7c38910", + "user": { + "id": "636fdc1b-8b86-47b0-8ea1-8bb7f7c38910", + "role": "user", + "created_at": "2020-01-23T00:52:39.43164Z", + "updated_at": "2020-01-23T00:52:39.798407Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/65.jpg", + "name": "Renee Sims" + }, + "type": "love", + "score": 1, + "created_at": "2020-01-23T00:52:40.173357Z", + "updated_at": "2020-01-23T00:52:40.173357Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "love": 1 + }, + "reaction_scores": { + "love": 1 + }, + "reply_count": 0, + "created_at": "2020-01-23T00:52:40.151326Z", + "updated_at": "2020-01-23T00:52:40.175705Z", + "mentioned_users": [] + }, + { + "id": "4dc8a789-3930-44b7-9707-7302209189fb", + "text": "https://www.sciencenews.org/article/peacock-spiders-superblack-spots-reflect-just-05-percent-light?tgt=nr", + "html": "\u003cp\u003e\u003ca href=\"https://www.sciencenews.org/article/peacock-spiders-superblack-spots-reflect-just-05-percent-light?tgt=nr\" rel=\"nofollow\"\u003ehttps://www.sciencenews.org/article/peacock-spiders-superblack-spots-reflect-just-05-percent-light?tgt=nr\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "636fdc1b-8b86-47b0-8ea1-8bb7f7c38910", + "role": "user", + "created_at": "2020-01-23T00:52:39.43164Z", + "updated_at": "2020-01-23T00:52:39.798407Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/65.jpg", + "name": "Renee Sims" + }, + "attachments": [ + { + "type": "image", + "author_name": "Science News", + "title": "Peacock spiders’ superblack spots reflect just 0.5 percent of light", + "title_link": "https://www.sciencenews.org/article/peacock-spiders-superblack-spots-reflect-just-05-percent-light", + "text": "By manipulating light with tiny structures, patches on peacock spiders appear superblack, helping accentuate the arachnids’ bright colors.", + "image_url": "https://www.sciencenews.org/wp-content/uploads/2019/05/051419_cw_spider_feat.jpg", + "thumb_url": "https://www.sciencenews.org/wp-content/uploads/2019/05/051419_cw_spider_feat.jpg", + "og_scrape_url": "https://www.sciencenews.org/article/peacock-spiders-superblack-spots-reflect-just-05-percent-light?tgt=nr" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-23T00:52:40.151921Z", + "updated_at": "2020-01-23T00:52:40.151921Z", + "mentioned_users": [] + }, + { + "id": "793722c6-5bbc-408c-8ba9-e1728e09d7c3", + "text": "https://www.youtube.com/watch?v=cJCtiJydw9U", + "html": "\u003cp\u003e\u003ca href=\"https://www.youtube.com/watch?v=cJCtiJydw9U\" rel=\"nofollow\"\u003ehttps://www.youtube.com/watch?v=cJCtiJydw9U\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "holy-sun-6", + "role": "user", + "created_at": "2020-01-23T00:52:39.412193Z", + "updated_at": "2020-01-23T00:52:40.211607Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=holy-sun-6\u0026name=Holy+sun", + "name": "Holy sun" + }, + "attachments": [ + { + "type": "video", + "author_name": "YouTube", + "title": "まるです11。-I am Maru 11.-", + "title_link": "https://www.youtube.com/watch?v=cJCtiJydw9U", + "text": "Happy 11th birthday!! BGM channel by h/mix -秋山裕和 公式チャンネル- https://www.youtube.com/channel/UCNPMwbX6-SclEmvFX5_ihCw Blog: http://sisinmaru.com/ Instagram: htt...", + "image_url": "https://i.ytimg.com/vi/cJCtiJydw9U/maxresdefault.jpg", + "thumb_url": "https://i.ytimg.com/vi/cJCtiJydw9U/maxresdefault.jpg", + "asset_url": "https://www.youtube.com/embed/cJCtiJydw9U", + "og_scrape_url": "https://www.youtube.com/watch?v=cJCtiJydw9U" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-23T00:52:40.193868Z", + "updated_at": "2020-01-23T00:52:40.193868Z", + "mentioned_users": [] + } + ], + "watcher_count": 1, + "read": [ + { + "user": { + "id": "636fdc1b-8b86-47b0-8ea1-8bb7f7c38910", + "role": "user", + "created_at": "2020-01-23T00:52:39.43164Z", + "updated_at": "2020-01-23T00:52:39.798407Z", + "banned": false, + "online": false, + "name": "Renee Sims", + "image": "https://randomuser.me/api/portraits/women/65.jpg" + }, + "last_read": "2020-01-23T00:52:39.570845184Z" + }, + { + "user": { + "id": "holy-sun-6", + "role": "user", + "created_at": "2020-01-23T00:52:39.412193Z", + "updated_at": "2020-01-23T00:52:40.211607Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=holy-sun-6\u0026name=Holy+sun", + "name": "Holy sun" + }, + "last_read": "2020-01-23T00:52:39.572206592Z" + } + ], + "members": [ + { + "user": { + "id": "636fdc1b-8b86-47b0-8ea1-8bb7f7c38910", + "role": "user", + "created_at": "2020-01-23T00:52:39.43164Z", + "updated_at": "2020-01-23T00:52:39.798407Z", + "banned": false, + "online": false, + "name": "Renee Sims", + "image": "https://randomuser.me/api/portraits/women/65.jpg" + }, + "role": "member", + "created_at": "2020-01-23T00:52:39.559506Z", + "updated_at": "2020-01-23T00:52:39.559506Z" + }, + { + "user": { + "id": "holy-sun-6", + "role": "user", + "created_at": "2020-01-23T00:52:39.412193Z", + "updated_at": "2020-01-23T00:52:40.211607Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=holy-sun-6\u0026name=Holy+sun", + "name": "Holy sun" + }, + "role": "owner", + "created_at": "2020-01-23T00:52:39.559506Z", + "updated_at": "2020-01-23T00:52:39.559506Z" + } + ] + } + ], + "duration": "30.85ms" +}'''; + final response = QueryChannelsResponse.fromJson(json.decode(jsonExample)); + expect(response.channels, isA>()); + }); + + test('ChannelStateResponse', () { + const jsonExample = r''' + { + "channel": { + "id": "!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "type": "messaging", + "cid": "messaging:!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "last_message_at": "2020-01-28T22:17:31.204287Z", + "created_at": "2020-01-28T22:17:31.00187Z", + "updated_at": "2020-01-28T22:17:31.00187Z", + "created_by": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "frozen": false, + "member_count": 2, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "Mia Denys", + "image": "https://randomuser.me/api/portraits/women/2.jpg" + }, + "messages": [ + { + "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "The Lion King Disney GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "text": "Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA" + } + ], + "latest_reactions": [ + { + "message_id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "type": "love", + "score": 1, + "created_at": "2020-01-28T22:17:31.128376Z", + "updated_at": "2020-01-28T22:17:31.128376Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "love": 1 + }, + "reaction_scores": { + "love": 1 + }, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.107978Z", + "updated_at": "2020-01-28T22:17:31.130506Z", + "mentioned_users": [] + }, + { + "id": "16e19c46-fb96-4f89-8031-d5bd2ce3bd64", + "text": "Few can name a topfull mother that isn't a breezeless damage.", + "html": "\u003cp\u003eFew can name a topfull mother that isn’t a breezeless damage.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.153518Z", + "updated_at": "2020-01-28T22:17:31.153518Z", + "mentioned_users": [] + }, + { + "id": "38b1b252-c9a6-4aea-a39e-8de3b7f7604b", + "text": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "Moustache Thumbs Up GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "text": "Discover \u0026 share this Pouce Leve GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "thumb_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.gif", + "asset_url": "https://media.giphy.com/media/73Sjhw0N4hyog/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/beard-muscle-73Sjhw0N4hyog" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.155428Z", + "updated_at": "2020-01-28T22:17:31.155428Z", + "mentioned_users": [] + }, + { + "id": "5c7d0c68-72d5-4027-b6b7-711b342d0fc4", + "text": "The carbons could be said to resemble smartish hoods.", + "html": "\u003cp\u003eThe carbons could be said to resemble smartish hoods.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.157811Z", + "updated_at": "2020-01-28T22:17:31.157811Z", + "mentioned_users": [] + }, + { + "id": "5b02535a-0c3c-45fa-9cf8-16f840c5123f", + "text": "Their software was, in this moment, a prolix feature.", + "html": "\u003cp\u003eTheir software was, in this moment, a prolix feature.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.158391Z", + "updated_at": "2020-01-28T22:17:31.158391Z", + "mentioned_users": [] + } + ], + "watcher_count": 1, + "read": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "last_read": "2020-01-28T22:17:31.016937728Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "last_read": "2020-01-28T22:17:31.018856448Z" + } + ], + "members": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "role": "member", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "role": "owner", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + } + ] + } + '''; + final response = ChannelStateResponse.fromJson(json.decode(jsonExample)); + expect(response.channel, isA()); + expect(response.watcherCount, isA()); + expect(response.members, isA>()); + expect(response.messages, isA>()); + expect(response.read, isA>()); + }); + + test('QueryUsersResponse', () { + const jsonExample = r''' + {"users":[{"id":"wild-breeze-7","role":"user","created_at":"2020-01-12T12:03:19.102029Z","updated_at":"2020-02-03T08:58:33.971562Z","last_active":"2020-02-03T08:58:33.965072Z","banned":false,"online":true,"name":"Wild breeze","image":"https://getstream.io/random_svg/?id=wild-breeze-7\u0026amp;name=Wild+breeze"}],"duration":"2.44ms"} + '''; + final response = QueryUsersResponse.fromJson(json.decode(jsonExample)); + expect(response.users, isA>()); + }); + + test('QueryReactionsResponse', () { + const jsonExample = r''' + {"reactions": [{"message_id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f","user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680","user": {"id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680","role": "user","created_at": "2020-01-28T22:17:30.83015Z","updated_at": "2020-01-28T22:17:31.19435Z","banned": false,"online": false,"image": "https://randomuser.me/api/portraits/women/2.jpg","name": "Mia Denys"},"type": "love","score": 1,"created_at": "2020-01-28T22:17:31.128376Z","updated_at": "2020-01-28T22:17:31.128376Z"}]} + '''; + final response = + QueryReactionsResponse.fromJson(json.decode(jsonExample)); + expect(response.reactions, isA>()); + }); + + test('QueryRepliesResponse', () { + const jsonExample = r''' + { "messages": [ + { + "id": "9db3ef01-e779-4279-8c54-ffd021eccec4", + "text": "A lustred seal is an alto of the mind.", + "html": "\u003cp\u003eA lustred seal is an alto of the mind.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.167579Z", + "updated_at": "2020-01-28T22:17:31.167579Z", + "mentioned_users": [] + }, + { + "id": "3232e92f-a96f-4b5e-bacb-3565e7155dc4", + "text": "https://giphy.com/gifs/netflix-marvel-the-punisher-l3mZsRS7ZfftbdLdS", + "html": "\u003cp\u003e\u003ca href=\"https://giphy.com/gifs/netflix-marvel-the-punisher-l3mZsRS7ZfftbdLdS\" rel=\"nofollow\"\u003ehttps://giphy.com/gifs/netflix-marvel-the-punisher-l3mZsRS7ZfftbdLdS\u003c/a\u003e\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "The Punisher Marvel GIF by NETFLIX - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/l3mZsRS7ZfftbdLdS/giphy.gif", + "text": "See What's Next in entertainment and Netflix original series, movies, TV, docs, and comedies. You can stream Netflix anytime, anywhere, on any device.", + "image_url": "https://media.giphy.com/media/l3mZsRS7ZfftbdLdS/giphy.gif", + "thumb_url": "https://media.giphy.com/media/l3mZsRS7ZfftbdLdS/giphy.gif", + "asset_url": "https://media.giphy.com/media/l3mZsRS7ZfftbdLdS/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/netflix-marvel-the-punisher-l3mZsRS7ZfftbdLdS" + } + ], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.168454Z", + "updated_at": "2020-01-28T22:17:31.168454Z", + "mentioned_users": [] + } + ]} + '''; + final response = QueryRepliesResponse.fromJson(json.decode(jsonExample)); + expect(response.messages, isA>()); + }); + + test('SearchMessagesResponse', () { + const jsonExample = r''' + { "results": [ + { + "message": { + "id": "9db3ef01-e779-4279-8c54-ffd021eccec4", + "text": "A lustred seal is an alto of the mind.", + "html": "\u003cp\u003eA lustred seal is an alto of the mind.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.167579Z", + "updated_at": "2020-01-28T22:17:31.167579Z", + "mentioned_users": [] + } + }] + } + '''; + final response = + SearchMessagesResponse.fromJson(json.decode(jsonExample)); + expect(response.results, isA>()); + }); + + test('ListDevicesResponse', () { + const jsonExample = + r'''{"devices":[{"push_provider":"firebase","id":"test"}],"duration":"0.35ms"}'''; + final response = ListDevicesResponse.fromJson(json.decode(jsonExample)); + expect(response.devices, isA>()); + }); + + test('SendFileResponse', () { + const jsonExample = r'''{"file": "file-url","duration":"0.35ms"}'''; + final response = SendFileResponse.fromJson(json.decode(jsonExample)); + expect(response.file, isA()); + }); + + test('SendImageResponse', () { + const jsonExample = r'''{"file": "file-url","duration":"0.35ms"}'''; + final response = SendImageResponse.fromJson(json.decode(jsonExample)); + expect(response.file, isA()); + }); + + test('SendImageResponse', () { + const jsonExample = r'''{"file": "file-url","duration":"0.35ms"}'''; + final response = SendImageResponse.fromJson(json.decode(jsonExample)); + expect(response.file, isA()); + }); + + test('EmptyResponse', () { + const jsonExample = r'''{"file": "file-url","duration":"0.35ms"}'''; + final response = EmptyResponse.fromJson(json.decode(jsonExample)); + expect(response.duration, isA()); + }); + + test('SendReactionResponse', () { + const jsonExample = r'''{"message": { + "id": "c6076f11-7768-4a04-bdf2-c43dddd6d666", + "text": "What we don't know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.", + "html": "\u003cp\u003eWhat we don’t know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.092262Z", + "updated_at": "2020-01-28T22:17:31.092262Z", + "mentioned_users": [] + }, + "reaction":{ + "message_id": "c74784e7-07ef-4b41-a8e3-b2b0e0b6b7ce", + "user_id": "spring-voice-7", + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "name": "Spring voice", + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice" + }, + "type": "sad", + "score": 1, + "created_at": "2020-01-28T22:17:31.131489Z", + "updated_at": "2020-01-28T22:17:31.131489Z" + },"duration":"0.35ms"}'''; + final response = SendReactionResponse.fromJson(json.decode(jsonExample)); + expect(response.message, isA()); + expect(response.reaction, isA()); + }); + + test('UpdateUsersResponse', () { + const jsonExample = + r'''{"users": {"bbb19d9a-ee50-45bc-84e5-0584e79d0c9e":{ + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }},"duration":"0.35ms"}'''; + final response = UpdateUsersResponse.fromJson(json.decode(jsonExample)); + expect(response.users, isA>()); + }); + + test('SetGuestUserResponse', () { + const jsonExample = + r'{"user":{"id":"guest-ac612aee-25fe-49fb-b1af-969e41f452a0-wild-breeze-7","role":"guest","created_at":"2020-02-03T10:19:01.538434Z","updated_at":"2020-02-03T10:19:01.539543Z","banned":false,"online":false},"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiZ3Vlc3QtYWM2MTJhZWUtMjVmZS00OWZiLWIxYWYtOTY5ZTQxZjQ1MmEwLXdpbGQtYnJlZXplLTcifQ.mmoFGu7oJjpFsp7nFN78UbIpO7gowbuIbyoppsuvbXA","duration":"4.66ms"}'; + final response = SetGuestUserResponse.fromJson(json.decode(jsonExample)); + expect(response.user, isA()); + expect(response.accessToken, isA()); + }); + + test('GetMessagesByIdResponse', () { + const jsonExample = r'''{"messages":[{ + "id": "c6076f11-7768-4a04-bdf2-c43dddd6d666", + "text": "What we don't know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.", + "html": "\u003cp\u003eWhat we don’t know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.092262Z", + "updated_at": "2020-01-28T22:17:31.092262Z", + "mentioned_users": [] + }],"duration":"4.66ms"}'''; + final response = + GetMessagesByIdResponse.fromJson(json.decode(jsonExample)); + expect(response.messages, isA>()); + }); + + test('SendActionResponse', () { + const jsonExample = r'''{"message":{ + "id": "c6076f11-7768-4a04-bdf2-c43dddd6d666", + "text": "What we don't know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.", + "html": "\u003cp\u003eWhat we don’t know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.092262Z", + "updated_at": "2020-01-28T22:17:31.092262Z", + "mentioned_users": [] + },"duration":"4.66ms"}'''; + final response = SendActionResponse.fromJson(json.decode(jsonExample)); + expect(response.message, isA()); + }); + + test('UpdateMessageResponse', () { + const jsonExample = r'''{"message":{ + "id": "c6076f11-7768-4a04-bdf2-c43dddd6d666", + "text": "What we don't know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.", + "html": "\u003cp\u003eWhat we don’t know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.092262Z", + "updated_at": "2020-01-28T22:17:31.092262Z", + "mentioned_users": [] + },"duration":"4.66ms"}'''; + final response = UpdateMessageResponse.fromJson(json.decode(jsonExample)); + expect(response.message, isA()); + }); + + test('SendMessageResponse', () { + const jsonExample = r'''{"message":{ + "id": "c6076f11-7768-4a04-bdf2-c43dddd6d666", + "text": "What we don't know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.", + "html": "\u003cp\u003eWhat we don’t know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.092262Z", + "updated_at": "2020-01-28T22:17:31.092262Z", + "mentioned_users": [] + },"duration":"4.66ms"}'''; + final response = SendMessageResponse.fromJson(json.decode(jsonExample)); + expect(response.message, isA()); + }); + + test('GetMessageResponse', () { + const jsonExample = r'''{"message":{ + "id": "c6076f11-7768-4a04-bdf2-c43dddd6d666", + "text": "What we don't know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.", + "html": "\u003cp\u003eWhat we don’t know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.092262Z", + "updated_at": "2020-01-28T22:17:31.092262Z", + "mentioned_users": [] + },"duration":"4.66ms"}'''; + final response = GetMessageResponse.fromJson(json.decode(jsonExample)); + expect(response.message, isA()); + }); + + test('UpdateChannelResponse', () { + const jsonExample = r'''{"message":{ + "id": "c6076f11-7768-4a04-bdf2-c43dddd6d666", + "text": "What we don't know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.", + "html": "\u003cp\u003eWhat we don’t know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.092262Z", + "updated_at": "2020-01-28T22:17:31.092262Z", + "mentioned_users": [] + }, + "members": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "role": "member", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "role": "owner", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + } + ], + "channel": { + "id": "!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "type": "messaging", + "cid": "messaging:!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "last_message_at": "2020-01-28T22:17:31.204287Z", + "created_at": "2020-01-28T22:17:31.00187Z", + "updated_at": "2020-01-28T22:17:31.00187Z", + "created_by": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "frozen": false, + "member_count": 2, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "Mia Denys", + "image": "https://randomuser.me/api/portraits/women/2.jpg" + }, + "duration":"4.66ms"}'''; + final response = UpdateChannelResponse.fromJson(json.decode(jsonExample)); + expect(response.channel, isA()); + expect(response.members, isA>()); + expect(response.message, isA()); + }); + + test('InviteMembersResponse', () { + const jsonExample = r'''{"message":{ + "id": "c6076f11-7768-4a04-bdf2-c43dddd6d666", + "text": "What we don't know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.", + "html": "\u003cp\u003eWhat we don’t know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.092262Z", + "updated_at": "2020-01-28T22:17:31.092262Z", + "mentioned_users": [] + }, + "members": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "role": "member", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "role": "owner", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + } + ], + "channel": { + "id": "!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "type": "messaging", + "cid": "messaging:!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "last_message_at": "2020-01-28T22:17:31.204287Z", + "created_at": "2020-01-28T22:17:31.00187Z", + "updated_at": "2020-01-28T22:17:31.00187Z", + "created_by": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "frozen": false, + "member_count": 2, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "Mia Denys", + "image": "https://randomuser.me/api/portraits/women/2.jpg" + }, + "duration":"4.66ms"}'''; + final response = InviteMembersResponse.fromJson(json.decode(jsonExample)); + expect(response.channel, isA()); + expect(response.members, isA>()); + expect(response.message, isA()); + }); + + test('RemoveMembersResponse', () { + const jsonExample = r'''{"message":{ + "id": "c6076f11-7768-4a04-bdf2-c43dddd6d666", + "text": "What we don't know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.", + "html": "\u003cp\u003eWhat we don’t know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.092262Z", + "updated_at": "2020-01-28T22:17:31.092262Z", + "mentioned_users": [] + }, + "members": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "role": "member", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "role": "owner", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + } + ], + "channel": { + "id": "!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "type": "messaging", + "cid": "messaging:!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "last_message_at": "2020-01-28T22:17:31.204287Z", + "created_at": "2020-01-28T22:17:31.00187Z", + "updated_at": "2020-01-28T22:17:31.00187Z", + "created_by": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "frozen": false, + "member_count": 2, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "Mia Denys", + "image": "https://randomuser.me/api/portraits/women/2.jpg" + }, + "duration":"4.66ms"}'''; + final response = RemoveMembersResponse.fromJson(json.decode(jsonExample)); + expect(response.channel, isA()); + expect(response.members, isA>()); + expect(response.message, isA()); + }); + + test('AddMembersResponse', () { + const jsonExample = r'''{"message":{ + "id": "c6076f11-7768-4a04-bdf2-c43dddd6d666", + "text": "What we don't know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.", + "html": "\u003cp\u003eWhat we don’t know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.092262Z", + "updated_at": "2020-01-28T22:17:31.092262Z", + "mentioned_users": [] + }, + "members": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "role": "member", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "role": "owner", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + } + ], + "channel": { + "id": "!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "type": "messaging", + "cid": "messaging:!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "last_message_at": "2020-01-28T22:17:31.204287Z", + "created_at": "2020-01-28T22:17:31.00187Z", + "updated_at": "2020-01-28T22:17:31.00187Z", + "created_by": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "frozen": false, + "member_count": 2, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "Mia Denys", + "image": "https://randomuser.me/api/portraits/women/2.jpg" + }, + "duration":"4.66ms"}'''; + final response = AddMembersResponse.fromJson(json.decode(jsonExample)); + expect(response.channel, isA()); + expect(response.members, isA>()); + expect(response.message, isA()); + }); + + test('AcceptInviteResponse', () { + const jsonExample = r'''{"message":{ + "id": "c6076f11-7768-4a04-bdf2-c43dddd6d666", + "text": "What we don't know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.", + "html": "\u003cp\u003eWhat we don’t know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.092262Z", + "updated_at": "2020-01-28T22:17:31.092262Z", + "mentioned_users": [] + }, + "members": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "role": "member", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "role": "owner", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + } + ], + "channel": { + "id": "!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "type": "messaging", + "cid": "messaging:!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "last_message_at": "2020-01-28T22:17:31.204287Z", + "created_at": "2020-01-28T22:17:31.00187Z", + "updated_at": "2020-01-28T22:17:31.00187Z", + "created_by": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "frozen": false, + "member_count": 2, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "Mia Denys", + "image": "https://randomuser.me/api/portraits/women/2.jpg" + }, + "duration":"4.66ms"}'''; + final response = AcceptInviteResponse.fromJson(json.decode(jsonExample)); + expect(response.channel, isA()); + expect(response.members, isA>()); + expect(response.message, isA()); + }); + + test('RejectInviteResponse', () { + const jsonExample = r'''{"message":{ + "id": "c6076f11-7768-4a04-bdf2-c43dddd6d666", + "text": "What we don't know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.", + "html": "\u003cp\u003eWhat we don’t know for sure is whether or not a step-daughter of the bear is assumed to be a farci hourglass.\u003c/p\u003e\n", + "type": "regular", + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg", + "name": "Robin Papa" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 1, + "created_at": "2020-01-28T22:17:31.092262Z", + "updated_at": "2020-01-28T22:17:31.092262Z", + "mentioned_users": [] + }, + "members": [ + { + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "role": "member", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + }, + { + "user": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "role": "owner", + "created_at": "2020-01-28T22:17:31.005135Z", + "updated_at": "2020-01-28T22:17:31.005135Z" + } + ], + "channel": { + "id": "!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "type": "messaging", + "cid": "messaging:!members-0LOcD0mZtTan60zHobLmELjdndXsonnBVNzZnB5mTt0", + "last_message_at": "2020-01-28T22:17:31.204287Z", + "created_at": "2020-01-28T22:17:31.00187Z", + "updated_at": "2020-01-28T22:17:31.00187Z", + "created_by": { + "id": "spring-voice-7", + "role": "user", + "created_at": "2020-01-28T22:17:30.834135Z", + "updated_at": "2020-01-28T22:17:31.186771Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?id=spring-voice-7\u0026name=Spring+voice", + "name": "Spring voice" + }, + "frozen": false, + "member_count": 2, + "config": { + "created_at": "2020-01-29T12:59:14.291912835Z", + "updated_at": "2020-01-29T12:59:14.291912991Z", + "name": "messaging", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "Mia Denys", + "image": "https://randomuser.me/api/portraits/women/2.jpg" + }, + "duration":"4.66ms"}'''; + final response = RejectInviteResponse.fromJson(json.decode(jsonExample)); + expect(response.channel, isA()); + expect(response.members, isA>()); + expect(response.message, isA()); + }); + }); +} diff --git a/packages/dart_client/test/src/api/web_socket_stub_test.dart b/packages/dart_client/test/src/api/web_socket_stub_test.dart new file mode 100644 index 00000000..5de61b4a --- /dev/null +++ b/packages/dart_client/test/src/api/web_socket_stub_test.dart @@ -0,0 +1,9 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/src/api/web_socket_channel_stub.dart'; + +void main() { + test('src/api/web_socket_stub_test', () { + expect( + () => connectWebSocket('fakeurl'), throwsA(isA())); + }); +} diff --git a/packages/dart_client/test/src/api/websocket_test.dart b/packages/dart_client/test/src/api/websocket_test.dart new file mode 100644 index 00000000..e5a88bd8 --- /dev/null +++ b/packages/dart_client/test/src/api/websocket_test.dart @@ -0,0 +1,353 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:logging/logging.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat/src/api/connection_status.dart'; +import 'package:stream_chat/src/api/websocket.dart'; +import 'package:stream_chat/src/models/event.dart'; +import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +class Functions { + WebSocketChannel connectFunc( + String url, { + Iterable protocols, + Map headers, + Duration pingInterval, + }) => + null; + + void handleFunc(Event event) => null; +} + +class MockFunctions extends Mock implements Functions {} + +class MockWSChannel extends Mock implements WebSocketChannel {} + +class MockWSSink extends Mock implements WebSocketSink {} + +void main() { + group('src/api/websocket', () { + test('should connect with correct parameters', () async { + final ConnectWebSocket connectFunc = MockFunctions().connectFunc; + + final ws = WebSocket( + baseUrl: 'baseurl', + user: User(id: 'testid'), + logger: Logger('ws'), + connectParams: {'test': 'true'}, + connectPayload: {'payload': 'test'}, + handler: (e) { + print(e); + }, + connectFunc: connectFunc, + ); + + final mockWSChannel = MockWSChannel(); + + final StreamController streamController = + StreamController.broadcast(); + + final computedUrl = + 'wss://baseurl/connect?test=true&json=%7B%22payload%22%3A%22test%22%2C%22user_details%22%3A%7B%22id%22%3A%22testid%22%7D%7D'; + + when(connectFunc(computedUrl)).thenAnswer((_) => mockWSChannel); + when(mockWSChannel.sink).thenAnswer((_) => MockWSSink()); + when(mockWSChannel.stream).thenAnswer((_) { + return streamController.stream; + }); + + final timer = Timer.periodic( + Duration(milliseconds: 100), + (_) => streamController.sink.add('{}'), + ); + + await ws.connect(); + + verify(connectFunc(computedUrl)).called(1); + expect(ws.connectionStatus.value, ConnectionStatus.connected); + + await streamController.close(); + timer.cancel(); + }); + }); + + test('should connect with correct parameters and handle events', () async { + final handleFunc = MockFunctions().handleFunc; + final ConnectWebSocket connectFunc = MockFunctions().connectFunc; + + final ws = WebSocket( + baseUrl: 'baseurl', + user: User(id: 'testid'), + logger: Logger('ws'), + connectParams: {'test': 'true'}, + connectPayload: {'payload': 'test'}, + handler: handleFunc, + connectFunc: connectFunc, + ); + + final mockWSChannel = MockWSChannel(); + + final StreamController streamController = + StreamController.broadcast(); + + final computedUrl = + 'wss://baseurl/connect?test=true&json=%7B%22payload%22%3A%22test%22%2C%22user_details%22%3A%7B%22id%22%3A%22testid%22%7D%7D'; + + when(connectFunc(computedUrl)).thenAnswer((_) => mockWSChannel); + when(mockWSChannel.sink).thenAnswer((_) => MockWSSink()); + when(mockWSChannel.stream).thenAnswer((_) { + return streamController.stream; + }); + + final connect = ws.connect().then((_) { + streamController.sink.add('{}'); + return Future.delayed(Duration(milliseconds: 200)); + }).then((value) { + verify(connectFunc(computedUrl)).called(1); + verify(handleFunc(any)).called(greaterThan(0)); + + return streamController.close(); + }); + + streamController.sink.add('{}'); + + return connect; + }); + + test('should close correctly the controller', () async { + final handleFunc = MockFunctions().handleFunc; + + final ConnectWebSocket connectFunc = MockFunctions().connectFunc; + + final ws = WebSocket( + baseUrl: 'baseurl', + user: User(id: 'testid'), + logger: Logger('ws'), + connectParams: {'test': 'true'}, + connectPayload: {'payload': 'test'}, + handler: handleFunc, + connectFunc: connectFunc, + ); + + final mockWSChannel = MockWSChannel(); + + final StreamController streamController = + StreamController.broadcast(); + + final computedUrl = + 'wss://baseurl/connect?test=true&json=%7B%22payload%22%3A%22test%22%2C%22user_details%22%3A%7B%22id%22%3A%22testid%22%7D%7D'; + + when(connectFunc(computedUrl)).thenAnswer((_) => mockWSChannel); + when(mockWSChannel.sink).thenAnswer((_) => MockWSSink()); + when(mockWSChannel.stream).thenAnswer((_) { + return streamController.stream; + }); + + final connect = ws.connect().then((_) { + streamController.sink.add('{}'); + return Future.delayed(Duration(milliseconds: 200)); + }).then((value) { + verify(connectFunc(computedUrl)).called(1); + verify(handleFunc(any)).called(greaterThan(0)); + + return streamController.close(); + }); + + streamController.sink.add('{}'); + + return connect; + }); + + test('should run correctly health check', () async { + final handleFunc = MockFunctions().handleFunc; + + final ConnectWebSocket connectFunc = MockFunctions().connectFunc; + + final ws = WebSocket( + baseUrl: 'baseurl', + user: User(id: 'testid'), + logger: Logger('ws'), + connectParams: {'test': 'true'}, + connectPayload: {'payload': 'test'}, + handler: handleFunc, + connectFunc: connectFunc, + ); + + final mockWSChannel = MockWSChannel(); + final mockWSSink = MockWSSink(); + + final StreamController streamController = + StreamController.broadcast(); + + final computedUrl = + 'wss://baseurl/connect?test=true&json=%7B%22payload%22%3A%22test%22%2C%22user_details%22%3A%7B%22id%22%3A%22testid%22%7D%7D'; + + when(connectFunc(computedUrl)).thenAnswer((_) => mockWSChannel); + when(mockWSChannel.stream).thenAnswer((_) { + return streamController.stream; + }); + when(mockWSChannel.sink).thenReturn(mockWSSink); + + final timer = Timer.periodic( + Duration(milliseconds: 1000), + (_) => streamController.sink.add('{}'), + ); + + final connect = ws.connect().then((_) { + streamController.sink.add('{}'); + return Future.delayed(Duration(milliseconds: 200)); + }).then((value) async { + verify(mockWSSink.add("{'type': 'health.check'}")).called(greaterThan(0)); + + timer.cancel(); + await streamController.close(); + return mockWSSink.close(); + }); + + streamController.sink.add('{}'); + + return connect; + }); + + test('should run correctly reconnection check', () async { + final handleFunc = MockFunctions().handleFunc; + + final ConnectWebSocket connectFunc = MockFunctions().connectFunc; + + Logger.root.level = Level.ALL; + final ws = WebSocket( + baseUrl: 'baseurl', + user: User(id: 'testid'), + logger: Logger('ws'), + connectParams: {'test': 'true'}, + connectPayload: {'payload': 'test'}, + handler: handleFunc, + connectFunc: connectFunc, + reconnectionMonitorTimeout: 1, + reconnectionMonitorInterval: 1, + ); + + final mockWSChannel = MockWSChannel(); + final mockWSSink = MockWSSink(); + + StreamController streamController = + StreamController.broadcast(); + + final computedUrl = + 'wss://baseurl/connect?test=true&json=%7B%22payload%22%3A%22test%22%2C%22user_details%22%3A%7B%22id%22%3A%22testid%22%7D%7D'; + + when(connectFunc(computedUrl)).thenAnswer((_) => mockWSChannel); + when(mockWSChannel.stream).thenAnswer((_) { + return streamController.stream; + }); + when(mockWSChannel.sink).thenReturn(mockWSSink); + + final connect = ws.connect().then((_) { + streamController.sink.add('{}'); + streamController.close(); + streamController = StreamController.broadcast(); + streamController.sink.add('{}'); + return Future.delayed(Duration(milliseconds: 200)); + }).then((value) async { + verify(mockWSSink.add("{'type': 'health.check'}")).called(greaterThan(0)); + + verify(connectFunc(computedUrl)).called(2); + + await streamController.close(); + return mockWSSink.close(); + }); + + streamController.sink.add('{}'); + + return connect; + }); + + test('should close correctly the controller', () async { + final handleFunc = MockFunctions().handleFunc; + + final ConnectWebSocket connectFunc = MockFunctions().connectFunc; + + final ws = WebSocket( + baseUrl: 'baseurl', + user: User(id: 'testid'), + logger: Logger('ws'), + connectParams: {'test': 'true'}, + connectPayload: {'payload': 'test'}, + handler: handleFunc, + connectFunc: connectFunc, + ); + + final mockWSChannel = MockWSChannel(); + final mockWSSink = MockWSSink(); + + final StreamController streamController = + StreamController.broadcast(); + + final computedUrl = + 'wss://baseurl/connect?test=true&json=%7B%22payload%22%3A%22test%22%2C%22user_details%22%3A%7B%22id%22%3A%22testid%22%7D%7D'; + + when(connectFunc(computedUrl)).thenAnswer((_) => mockWSChannel); + when(mockWSChannel.stream).thenAnswer((_) { + return streamController.stream; + }); + when(mockWSChannel.sink).thenReturn(mockWSSink); + + final connect = ws.connect().then((_) { + streamController.sink.add('{}'); + return Future.delayed(Duration(milliseconds: 200)); + }).then((value) async { + await ws.disconnect(); + verify(mockWSSink.close()).called(greaterThan(0)); + + await streamController.close(); + await mockWSSink.close(); + }); + + streamController.sink.add('{}'); + + return connect; + }); + + test('should throw an error', () async { + final ConnectWebSocket connectFunc = MockFunctions().connectFunc; + + final ws = WebSocket( + baseUrl: 'baseurl', + user: User(id: 'testid'), + logger: Logger('ws'), + connectParams: {'test': 'true'}, + connectPayload: {'payload': 'test'}, + handler: (e) { + print(e); + }, + connectFunc: connectFunc, + ); + + final mockWSChannel = MockWSChannel(); + + final streamController = StreamController.broadcast(); + + final computedUrl = + 'wss://baseurl/connect?test=true&json=%7B%22payload%22%3A%22test%22%2C%22user_details%22%3A%7B%22id%22%3A%22testid%22%7D%7D'; + + when(connectFunc(computedUrl)).thenAnswer((_) => mockWSChannel); + when(mockWSChannel.sink).thenAnswer((_) => MockWSSink()); + when(mockWSChannel.stream).thenAnswer((_) { + return streamController.stream; + }); + + Future.delayed( + Duration(milliseconds: 1000), + () => streamController.sink.addError('test error'), + ); + + try { + expect(await ws.connect(), throwsA(isA())); + } catch (e) { + verify(connectFunc(computedUrl)).called(greaterThanOrEqualTo(1)); + } + }); +} diff --git a/packages/dart_client/test/src/client_test.dart b/packages/dart_client/test/src/client_test.dart new file mode 100644 index 00000000..738255cc --- /dev/null +++ b/packages/dart_client/test/src/client_test.dart @@ -0,0 +1,979 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:dio/native_imp.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:logging/logging.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stream_chat/src/api/requests.dart'; +import 'package:stream_chat/src/client.dart'; +import 'package:stream_chat/src/exceptions.dart'; +import 'package:stream_chat/src/models/message.dart'; +import 'package:stream_chat/src/models/user.dart'; + +class MockDio extends Mock implements DioForNative {} + +class MockHttpClientAdapter extends Mock implements HttpClientAdapter {} + +class Functions { + Future tokenProvider(String userId) => null; +} + +class MockFunctions extends Mock implements Functions {} + +void main() { + group('src/client', () { + group('constructor', () { + final List log = []; + + overridePrint(testFn()) => () { + log.clear(); + final spec = ZoneSpecification(print: (_, __, ___, String msg) { + // Add to log instead of printing to stdout + log.add(msg); + }); + return Zone.current.fork(specification: spec).run(testFn); + }; + + tearDown(() { + log.clear(); + }); + + test('should create the object correctly', () { + final client = Client( + 'api-key', + persistenceEnabled: false, + ); + + expect(client.baseURL, 'chat-us-east-1.stream-io-api.com'); + expect(client.apiKey, 'api-key'); + expect(client.logLevel, Level.WARNING); + expect(client.httpClient.options.connectTimeout, 6000); + expect(client.httpClient.options.receiveTimeout, 6000); + }); + + test('should create the object correctly', overridePrint(() { + final LogHandlerFunction logHandler = (LogRecord record) { + print(record.message); + }; + + final client = Client( + 'api-key', + persistenceEnabled: false, + connectTimeout: Duration(seconds: 10), + receiveTimeout: Duration(seconds: 12), + logLevel: Level.INFO, + baseURL: 'test.com', + logHandlerFunction: logHandler, + ); + + expect(client.baseURL, 'test.com'); + expect(client.apiKey, 'api-key'); + expect(Logger.root.level, Level.INFO); + expect(client.httpClient.options.connectTimeout, 10000); + expect(client.httpClient.options.receiveTimeout, 12000); + + client.logger.warning('test'); + client.logger.config('test config'); + + expect([log[log.length - 2], log[log.length - 1]], + ['instantiating new client', 'test']); + })); + + test('Channel', () { + final client = Client( + 'test', + persistenceEnabled: false, + ); + final Map data = {'test': 1}; + final channelClient = client.channel('type', id: 'id', extraData: data); + expect(channelClient.type, 'type'); + expect(channelClient.id, 'id'); + }); + }); + + group('queryChannels', () { + test('should pass right default parameters', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + persistenceEnabled: false, + ); + + final queryParams = { + 'payload': json.encode({ + "filter_conditions": null, + "sort": null, + "state": true, + "watch": true, + "presence": false, + "limit": 10, + }), + }; + + when(mockDio.get('/channels', queryParameters: queryParams)) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.queryChannels(waitForConnect: false); + + verify(mockDio.get('/channels', queryParameters: queryParams)) + .called(1); + }); + + test('should pass right parameters', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + httpClient: mockDio, + persistenceEnabled: false, + ); + + final queryFilter = { + "id": { + "\$in": ["test"], + }, + }; + final sortOptions = []; + final options = {"state": false, "watch": false, "presence": true}; + final paginationParams = PaginationParams( + limit: 10, + offset: 2, + ); + + final queryParams = { + 'payload': json.encode({ + "filter_conditions": queryFilter, + "sort": sortOptions, + } + ..addAll(options) + ..addAll(paginationParams.toJson())), + }; + + when(mockDio.get('/channels', queryParameters: queryParams)) + .thenAnswer((_) async { + return Response(data: '{}', statusCode: 200); + }); + + await client.queryChannels( + filter: queryFilter, + sort: sortOptions, + options: options, + paginationParams: paginationParams, + waitForConnect: false, + ); + + verify(mockDio.get('/channels', queryParameters: queryParams)) + .called(1); + }); + }); + + group('search', () { + test('should pass right default parameters', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + final queryParams = { + 'payload': json.encode({ + "filter_conditions": null, + 'query': null, + 'sort': null, + }), + }; + + when(mockDio.get('/search', queryParameters: queryParams)) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.search(null, null, null, null); + + verify(mockDio.get('/search', queryParameters: queryParams)) + .called(1); + }); + + test('should pass right parameters', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + final filters = { + "id": { + "\$in": ["test"], + }, + }; + final sortOptions = [SortOption('name')]; + final query = 'query'; + + final queryParams = { + 'payload': json.encode({ + "filter_conditions": filters, + 'query': query, + 'sort': sortOptions, + "limit": 10, + }), + }; + + when(mockDio.get('/search', queryParameters: queryParams)) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.search( + filters, + sortOptions, + query, + PaginationParams(), + ); + + verify(mockDio.get('/search', queryParameters: queryParams)) + .called(1); + }); + }); + + group('devices', () { + test('addDevice', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + when(mockDio.post('/devices', data: { + 'id': 'test-id', + 'push_provider': 'test', + })).thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.addDevice('test-id', 'test'); + + verify(mockDio.post('/devices', + data: {'id': 'test-id', 'push_provider': 'test'})).called(1); + }); + + test('getDevices', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + when(mockDio.get('/devices')) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.getDevices(); + + verify(mockDio.get('/devices')).called(1); + }); + + test('removeDevice', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + when(mockDio + .delete('/devices', queryParameters: {'id': 'test-id'})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.removeDevice('test-id'); + + verify(mockDio.delete('/devices', + queryParameters: {'id': 'test-id'})).called(1); + }); + }); + + test('devToken', () { + final client = Client( + 'api-key', + persistenceEnabled: false, + ); + final token = client.devToken('test'); + + expect( + token, + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidGVzdCJ9.devtoken', + ); + }); + + group('queryUsers', () { + test('should pass right default parameters', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + final queryParams = { + 'payload': json.encode({ + "filter_conditions": {}, + "sort": null, + "presence": false, + }), + }; + + when(mockDio.get('/users', queryParameters: queryParams)) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.queryUsers(); + + verify(mockDio.get('/users', queryParameters: queryParams)) + .called(1); + }); + + test('should pass right parameters', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + final Map queryFilter = { + "id": { + "\$in": ["test"], + }, + }; + final List sortOptions = []; + final options = {"presence": true}; + + final Map queryParams = { + 'payload': json.encode({ + "filter_conditions": queryFilter, + "sort": sortOptions, + }..addAll(options)), + }; + + when(mockDio.get('/users', queryParameters: queryParams)) + .thenAnswer((_) async { + return Response(data: '{}', statusCode: 200); + }); + + await client.queryUsers( + filter: queryFilter, + sort: sortOptions, + options: options, + ); + + verify(mockDio.get('/users', queryParameters: queryParams)) + .called(1); + }); + }); + + group('user', () { + test('setUser should throw exception', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + when(mockDio.post('/moderation/flag', + data: {'target_user_id': 'test-id'})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.flagUser('test-id'); + + verify(mockDio.post('/moderation/flag', + data: {'target_user_id': 'test-id'})).called(1); + }); + + test('flagUser', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + expect(() => client.setUserWithProvider(User(id: 'test-id')), + throwsA(isA())); + }); + + test('unflagUser', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + when(mockDio.post('/moderation/unflag', + data: {'target_user_id': 'test-id'})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.unflagUser('test-id'); + + verify(mockDio.post('/moderation/unflag', + data: {'target_user_id': 'test-id'})).called(1); + }); + + test('updateUser', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + final user = User(id: 'test-id'); + + final data = { + 'users': {user.id: user.toJson()}, + }; + + when(mockDio.post('/users', data: data)) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.updateUser(user); + + verify(mockDio.post('/users', data: data)).called(1); + }); + + test('updateUsers', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + final user = User(id: 'test-id'); + final user2 = User(id: 'test-id2'); + + final data = { + 'users': { + user.id: user.toJson(), + user2.id: user2.toJson(), + }, + }; + + when(mockDio.post('/users', data: data)) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.updateUsers([user, user2]); + + verify(mockDio.post('/users', data: data)).called(1); + }); + + test('banUser', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + when(mockDio.post('/moderation/ban', + data: {'test': true, 'target_user_id': 'test-id'})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.banUser('test-id', {'test': true}); + + verify(mockDio.post('/moderation/ban', + data: {'test': true, 'target_user_id': 'test-id'})).called(1); + }); + + test('unbanUser', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + when(mockDio.delete('/moderation/ban', + queryParameters: {'test': true, 'target_user_id': 'test-id'})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.unbanUser('test-id', {'test': true}); + + verify(mockDio.delete('/moderation/ban', + queryParameters: {'test': true, 'target_user_id': 'test-id'})) + .called(1); + }); + + test('muteUser', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + when(mockDio.post('/moderation/mute', + data: {'target_id': 'test-id'})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.muteUser('test-id'); + + verify(mockDio.post('/moderation/mute', + data: {'target_id': 'test-id'})).called(1); + }); + + test('unmuteUser', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + when(mockDio.post('/moderation/unmute', + data: {'target_id': 'test-id'})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.unmuteUser('test-id'); + + verify(mockDio.post('/moderation/unmute', + data: {'target_id': 'test-id'})).called(1); + }); + }); + + group('message', () { + test('flagMessage', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + when(mockDio.post('/moderation/flag', + data: {'target_message_id': 'test-id'})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.flagMessage('test-id'); + + verify(mockDio.post('/moderation/flag', + data: {'target_message_id': 'test-id'})).called(1); + }); + + test('unflagMessage', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + when(mockDio.post('/moderation/unflag', + data: {'target_message_id': 'test-id'})) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.unflagMessage('test-id'); + + verify(mockDio.post('/moderation/unflag', + data: {'target_message_id': 'test-id'})).called(1); + }); + + test('updateMessage', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + final message = Message( + id: 'test', + updatedAt: DateTime.now(), + ); + + when(mockDio.post( + '/messages/${message.id}', + data: {'message': message}, + )).thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.updateMessage(message); + + verify(mockDio.post('/messages/${message.id}', + data: {'message': anything})).called(1); + }); + + test('deleteMessage', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + final messageId = 'test'; + + when(mockDio.delete('/messages/$messageId')) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.deleteMessage(Message(id: messageId)); + + verify(mockDio.delete('/messages/$messageId')).called(1); + }); + + test('getMessage', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + final messageId = 'test'; + + when(mockDio.get('/messages/$messageId')) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.getMessage(messageId); + + verify(mockDio.get('/messages/$messageId')).called(1); + }); + + test('markAllRead', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + when(mockDio.post('/channels/read')) + .thenAnswer((_) async => Response(data: '{}', statusCode: 200)); + + await client.markAllRead(); + + verify(mockDio.post('/channels/read')).called(1); + }); + }); + + group('api methods', () { + group('get', () { + test('should put the correct parameters', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + final Map queryParams = { + 'test': 1, + }; + + when(mockDio.get('/test', queryParameters: queryParams)) + .thenAnswer((_) async { + return Response(data: '{}', statusCode: 200); + }); + + await client.get('/test', queryParameters: queryParams); + + verify(mockDio.get('/test', queryParameters: queryParams)) + .called(1); + }); + + test('should catch the error', () async { + final dioHttp = Dio(); + final mockHttpClientAdapter = MockHttpClientAdapter(); + dioHttp.httpClientAdapter = mockHttpClientAdapter; + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: dioHttp, + ); + + when(mockHttpClientAdapter.fetch(any, any, any)).thenAnswer( + (_) async => ResponseBody.fromString('test error', 400)); + + expect(client.get('/test'), throwsA(ApiError('test error', 400))); + }); + }); + + group('post', () { + test('should put the correct parameters', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + final Map data = { + 'test': 1, + }; + + when(mockDio.post('/test', data: data)).thenAnswer((_) async { + return Response(data: '{}', statusCode: 200); + }); + + await client.post('/test', data: data); + + verify(mockDio.post('/test', data: data)).called(1); + }); + + test('should catch the error', () async { + final dioHttp = Dio(); + final mockHttpClientAdapter = MockHttpClientAdapter(); + dioHttp.httpClientAdapter = mockHttpClientAdapter; + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: dioHttp, + ); + + when(mockHttpClientAdapter.fetch(any, any, any)).thenAnswer( + (_) async => ResponseBody.fromString('test error', 400)); + + expect(client.post('/test'), throwsA(ApiError('test error', 400))); + }); + }); + + group('put', () { + test('should put the correct parameters', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + final Map data = { + 'test': 1, + }; + + when(mockDio.put('/test', data: data)).thenAnswer((_) async { + return Response(data: '{}', statusCode: 200); + }); + + await client.put('/test', data: data); + + verify(mockDio.put('/test', data: data)).called(1); + }); + + test('should catch the error', () async { + final dioHttp = Dio(); + final mockHttpClientAdapter = MockHttpClientAdapter(); + dioHttp.httpClientAdapter = mockHttpClientAdapter; + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: dioHttp, + ); + + when(mockHttpClientAdapter.fetch(any, any, any)).thenAnswer( + (_) async => ResponseBody.fromString('test error', 400)); + + expect(client.put('/test'), throwsA(ApiError('test error', 400))); + }); + }); + + group('patch', () { + test('should put the correct parameters', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + final Map data = { + 'test': 1, + }; + + when(mockDio.patch('/test', data: data)) + .thenAnswer((_) async { + return Response(data: '{}', statusCode: 200); + }); + + await client.patch('/test', data: data); + + verify(mockDio.patch('/test', data: data)).called(1); + }); + + test('should catch the error', () async { + final dioHttp = Dio(); + final mockHttpClientAdapter = MockHttpClientAdapter(); + dioHttp.httpClientAdapter = mockHttpClientAdapter; + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: dioHttp, + ); + + when(mockHttpClientAdapter.fetch(any, any, any)).thenAnswer( + (_) async => ResponseBody.fromString('test error', 400)); + + expect(client.patch('/test'), throwsA(ApiError('test error', 400))); + }); + }); + + group('delete', () { + test('should put the correct parameters', () async { + final mockDio = MockDio(); + + when(mockDio.options).thenReturn(BaseOptions()); + when(mockDio.interceptors).thenReturn(Interceptors()); + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: mockDio, + ); + + final Map queryParams = { + 'test': 1, + }; + + when(mockDio.delete('/test', queryParameters: queryParams)) + .thenAnswer((_) async { + return Response(data: '{}', statusCode: 200); + }); + + await client.delete('/test', queryParameters: queryParams); + + verify(mockDio.delete('/test', queryParameters: queryParams)) + .called(1); + }); + + test('should catch the error', () async { + final dioHttp = Dio(); + final mockHttpClientAdapter = MockHttpClientAdapter(); + dioHttp.httpClientAdapter = mockHttpClientAdapter; + + final client = Client( + 'api-key', + persistenceEnabled: false, + httpClient: dioHttp, + ); + + when(mockHttpClientAdapter.fetch(any, any, any)).thenAnswer( + (_) async => ResponseBody.fromString('test error', 400)); + + expect(client.delete('/test'), throwsA(ApiError('test error', 400))); + }); + }); + }); + }); +} diff --git a/packages/dart_client/test/src/models/action_test.dart b/packages/dart_client/test/src/models/action_test.dart new file mode 100644 index 00000000..2f75dddb --- /dev/null +++ b/packages/dart_client/test/src/models/action_test.dart @@ -0,0 +1,46 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/src/models/action.dart'; + +void main() { + group('src/models/action', () { + const jsonExample = r'''{ + "name": "name", + "style": "style", + "text": "text", + "type": "type", + "value": "value" + }'''; + + test('should parse json correctly', () { + final action = Action.fromJson(json.decode(jsonExample)); + expect(action.name, 'name'); + expect(action.style, 'style'); + expect(action.text, 'text'); + expect(action.type, 'type'); + expect(action.value, 'value'); + }); + + test('should serialize to json correctly', () { + final action = Action( + name: 'name', + style: 'style', + text: 'text', + type: 'type', + value: 'value', + ); + + expect( + action.toJson(), + { + 'name': 'name', + 'style': 'style', + 'text': 'text', + 'type': 'type', + 'value': 'value', + }, + ); + }); + }); +} diff --git a/packages/dart_client/test/src/models/attachment_test.dart b/packages/dart_client/test/src/models/attachment_test.dart new file mode 100644 index 00000000..eaa8e9cc --- /dev/null +++ b/packages/dart_client/test/src/models/attachment_test.dart @@ -0,0 +1,69 @@ +import 'package:stream_chat/src/models/attachment.dart'; +import 'package:stream_chat/src/models/action.dart'; +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('src/models/attachment', () { + const jsonExample = r'''{ + "type": "giphy", + "title": "awesome", + "title_link": "https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti", + "thumb_url": "https://media0.giphy.com/media/3o7TKnCdBx5cMg0qti/giphy.gif", + "actions": [ + { + "name": "image_action", + "text": "Send", + "style": "primary", + "type": "button", + "value": "send" + }, + { + "name": "image_action", + "text": "Shuffle", + "style": "default", + "type": "button", + "value": "shuffle" + }, + { + "name": "image_action", + "text": "Cancel", + "style": "default", + "type": "button", + "value": "cancel" + } + ] +}'''; + + test('should parse json correctly', () { + final attachment = Attachment.fromJson(json.decode(jsonExample)); + expect(attachment.type, "giphy"); + expect(attachment.title, "awesome"); + expect(attachment.titleLink, + "https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti"); + expect(attachment.thumbUrl, + "https://media0.giphy.com/media/3o7TKnCdBx5cMg0qti/giphy.gif"); + expect(attachment.actions, hasLength(3)); + expect(attachment.actions[0], isA()); + }); + + test('should serialize to json correctly', () { + final channel = Attachment( + type: "image", + title: "soo", + titleLink: + "https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti"); + + expect( + channel.toJson(), + { + 'type': 'image', + 'title': 'soo', + 'title_link': + 'https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti' + }, + ); + }); + }); +} diff --git a/packages/dart_client/test/src/models/channel_state_test.dart b/packages/dart_client/test/src/models/channel_state_test.dart new file mode 100644 index 00000000..21ffb20c --- /dev/null +++ b/packages/dart_client/test/src/models/channel_state_test.dart @@ -0,0 +1,1112 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/src/models/channel_config.dart'; +import 'package:stream_chat/src/models/channel_state.dart'; +import 'package:stream_chat/src/models/command.dart'; +import 'package:stream_chat/src/models/message.dart'; +import 'package:stream_chat/src/models/user.dart'; +import 'package:stream_chat/stream_chat.dart'; + +void main() { + group('src/models/channel_state', () { + const jsonExample = r'''{ + "channel": { + "id": "dev", + "type": "team", + "cid": "team:dev", + "last_message_at": "2020-01-30T13:43:41.062362Z", + "created_at": "2019-04-03T18:43:33.213373Z", + "updated_at": "2019-04-03T18:43:33.213374Z", + "team": "test", + "created_by": { + "id": "guido", + "role": "user", + "created_at": "2019-04-03T18:43:33.201036Z", + "updated_at": "2019-04-03T18:43:33.204713Z", + "banned": false, + "online": false, + "name": "Guido" + }, + "frozen": true, + "config": { + "created_at": "2019-11-07T22:29:26.776526Z", + "updated_at": "2019-11-07T22:29:48.286746Z", + "name": "team", + "typing_events": true, + "read_events": true, + "connect_events": true, + "search": true, + "reactions": true, + "replies": true, + "mutes": true, + "uploads": true, + "url_enrichment": true, + "message_retention": "infinite", + "max_message_length": 5000, + "automod": "disabled", + "automod_behavior": "flag", + "commands": [ + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + "set": "fun_set" + } + ] + }, + "name": "#dev", + "image": "https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png", + "example": 1 + }, + "messages": [ + { + "id": "dry-meadow-0-2b73cc8b-cd86-4a01-8d40-bd82ad07a030", + "text": "fasdfa", + "type": "regular", + "status": "SENT", + "silent": false, + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:23:02.843948Z", + "updated_at": "2020-01-29T03:23:02.843949Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "dry-meadow-0-e8e74482-b4cd-48db-9d1e-30e6c191786f", + "text": "test message", + "type": "regular", + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:23:07.981091Z", + "updated_at": "2020-01-29T03:23:07.981091Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "dry-meadow-0-53e6299f-9b97-4a9c-a27e-7e2dde49b7e0", + "text": "test message", + "type": "regular", + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:23:11.568022Z", + "updated_at": "2020-01-29T03:23:11.568022Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "dry-meadow-0-80925be0-786e-40a5-b225-486518dafd35", + "text": "asdfadf", + "type": "regular", + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:32:57.403566Z", + "updated_at": "2020-01-29T03:32:57.403566Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "dry-meadow-0-64d7970f-ede8-4b31-9738-1bc1756d2bfe", + "text": "test", + "type": "regular", + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:33:35.294802Z", + "updated_at": "2020-01-29T03:33:35.294802Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "withered-cell-0-84cbd760-cf55-4f7e-9207-c5f66cccc6dc", + "text": "hi", + "type": "regular", + "user": { + "id": "withered-cell-0", + "role": "user", + "created_at": "2020-01-29T03:34:01.698106Z", + "updated_at": "2020-01-29T03:34:01.708808Z", + "last_active": "2020-01-29T03:34:01.70353Z", + "banned": false, + "online": false, + "name": "Withered cell", + "image": "https://getstream.io/random_svg/?name=Withered+cell" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:34:27.393296Z", + "updated_at": "2020-01-29T03:34:27.393296Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "dry-meadow-0-e9203588-43c3-40b1-91f7-f217fc42aa53", + "text": "fantastic", + "type": "regular", + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:34:37.638376Z", + "updated_at": "2020-01-29T03:34:37.638376Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "withered-cell-0-7e3552d7-7a0d-45f2-a856-e91b23a7e240", + "text": "nice to meet you", + "type": "regular", + "user": { + "id": "withered-cell-0", + "role": "user", + "created_at": "2020-01-29T03:34:01.698106Z", + "updated_at": "2020-01-29T03:34:01.708808Z", + "last_active": "2020-01-29T03:34:01.70353Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Withered+cell", + "name": "Withered cell" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:35:04.301566Z", + "updated_at": "2020-01-29T03:35:04.301566Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "dry-meadow-0-1ffeafd4-e4fc-4c84-9394-9d7cb10fff42", + "text": "hey", + "type": "regular", + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:35:24.939084Z", + "updated_at": "2020-01-29T03:35:24.939085Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "dry-meadow-0-3f147324-12c8-4b41-9fb5-2db88d065efa", + "text": "hello, everyone", + "type": "regular", + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "name": "Dry meadow", + "image": "https://getstream.io/random_svg/?name=Dry+meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:35:33.101566Z", + "updated_at": "2020-01-29T03:35:33.101566Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "dry-meadow-0-51a348ae-0c0a-44de-a556-eac7891c0cf0", + "text": "who is there?", + "type": "regular", + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "name": "Dry meadow", + "image": "https://getstream.io/random_svg/?name=Dry+meadow" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T03:35:45.458685Z", + "updated_at": "2020-01-29T03:35:45.458685Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "icy-recipe-7-a29e237b-8d81-4a97-9bc8-d42bca3f1356", + "text": "하이", + "type": "regular", + "user": { + "id": "icy-recipe-7", + "role": "user", + "created_at": "2020-01-21T11:36:22.284503Z", + "updated_at": "2020-01-29T07:01:59.69882Z", + "last_active": "2020-01-29T07:01:59.693378Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Icy+recipe", + "name": "Icy recipe" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T07:02:11.535395Z", + "updated_at": "2020-01-29T07:02:11.535395Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "icy-recipe-7-935c396e-ddf8-4a9a-951c-0a12fa5bf055", + "text": "what are you doing?", + "type": "regular", + "user": { + "id": "icy-recipe-7", + "role": "user", + "created_at": "2020-01-21T11:36:22.284503Z", + "updated_at": "2020-01-29T07:01:59.69882Z", + "last_active": "2020-01-29T07:01:59.693378Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Icy+recipe", + "name": "Icy recipe" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T07:02:22.485136Z", + "updated_at": "2020-01-29T07:02:22.485136Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "throbbing-boat-5-1e4d5730-5ff0-4d25-9948-9f34ffda43e4", + "text": "👍", + "type": "regular", + "user": { + "id": "throbbing-boat-5", + "role": "user", + "created_at": "2019-07-30T06:29:53.060413Z", + "updated_at": "2020-01-29T14:11:27.80176Z", + "last_active": "2020-01-29T14:11:27.7963Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Throbbing+boat", + "name": "Throbbing boat" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T14:12:04.688552Z", + "updated_at": "2020-01-29T14:12:04.688552Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "snowy-credit-3-3e0c1a0d-d22f-42ee-b2a1-f9f49477bf21", + "text": "sdasas", + "type": "regular", + "user": { + "id": "snowy-credit-3", + "role": "user", + "created_at": "2020-01-29T15:29:03.693312Z", + "updated_at": "2020-01-29T15:29:03.702648Z", + "last_active": "2020-01-29T15:29:03.696144Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Snowy+credit", + "name": "Snowy credit" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T15:29:36.011315Z", + "updated_at": "2020-01-29T15:29:36.011316Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "snowy-credit-3-3319537e-2d0e-4876-8170-a54f046e4b7d", + "text": "cjshsa", + "type": "regular", + "user": { + "id": "snowy-credit-3", + "role": "user", + "created_at": "2020-01-29T15:29:03.693312Z", + "updated_at": "2020-01-29T15:29:03.702648Z", + "last_active": "2020-01-29T15:29:03.696144Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Snowy+credit", + "name": "Snowy credit" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T15:29:41.677819Z", + "updated_at": "2020-01-29T15:29:41.677819Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "snowy-credit-3-cfaf0b46-1daa-49c5-947c-b16d6697487d", + "text": "nhisagdhsadz", + "type": "regular", + "user": { + "id": "snowy-credit-3", + "role": "user", + "created_at": "2020-01-29T15:29:03.693312Z", + "updated_at": "2020-01-29T15:29:03.702648Z", + "last_active": "2020-01-29T15:29:03.696144Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Snowy+credit", + "name": "Snowy credit" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T15:29:43.354177Z", + "updated_at": "2020-01-29T15:29:43.354177Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "snowy-credit-3-cebe25a7-a3a3-49fc-9919-91c6725e81f3", + "text": "hvadhsahzd", + "type": "regular", + "user": { + "id": "snowy-credit-3", + "role": "user", + "created_at": "2020-01-29T15:29:03.693312Z", + "updated_at": "2020-01-29T15:29:03.702648Z", + "last_active": "2020-01-29T15:29:03.696144Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Snowy+credit", + "name": "Snowy credit" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T15:29:44.754713Z", + "updated_at": "2020-01-29T15:29:44.754713Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "divine-glade-9-0cea9262-5766-48e9-8b22-311870aed3bf", + "text": "hello", + "type": "regular", + "user": { + "id": "divine-glade-9", + "role": "user", + "created_at": "2020-01-29T17:02:18.312524Z", + "updated_at": "2020-01-29T17:02:18.320187Z", + "last_active": "2020-01-29T17:02:18.315074Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Divine+glade", + "name": "Divine glade" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T17:02:36.933852Z", + "updated_at": "2020-01-29T17:02:36.933852Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "red-firefly-9-c4e9007b-bb7d-4238-ae08-5f8e3cd03d73", + "text": "hello", + "type": "regular", + "user": { + "id": "red-firefly-9", + "role": "user", + "created_at": "2019-08-02T18:56:39.366516Z", + "updated_at": "2020-01-29T22:13:50.491769Z", + "last_active": "2020-01-29T22:13:50.450215Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Red+firefly", + "name": "Red firefly" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-29T22:14:08.54062Z", + "updated_at": "2020-01-29T22:14:08.54062Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "bitter-glade-2-02aee4eb-4093-4736-808b-2de75820e854", + "text": "hello", + "type": "regular", + "user": { + "id": "bitter-glade-2", + "role": "user", + "created_at": "2020-01-30T13:08:56.190678Z", + "updated_at": "2020-01-30T13:08:56.200333Z", + "last_active": "2020-01-30T13:08:56.193882Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Bitter+glade", + "name": "Bitter glade" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-30T13:11:37.191293Z", + "updated_at": "2020-01-30T13:11:37.191293Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "morning-sea-1-0c700bcb-46dd-4224-b590-e77bdbccc480", + "text": "http://jaeger.ui.gtstrm.com/", + "type": "regular", + "user": { + "id": "morning-sea-1", + "role": "user", + "created_at": "2019-07-22T09:19:07.505207Z", + "updated_at": "2020-01-30T13:33:05.831856Z", + "last_active": "2020-01-30T13:33:05.825369Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Morning+sea", + "name": "Morning sea" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-30T13:33:16.853116Z", + "updated_at": "2020-01-30T13:33:16.853116Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "ancient-salad-0-53e8b4e6-5b7b-43ad-aeee-8bfb6a9ed0be", + "text": "hi", + "type": "regular", + "user": { + "id": "ancient-salad-0", + "role": "user", + "created_at": "2020-01-30T13:34:29.286813Z", + "updated_at": "2020-01-30T13:34:29.296196Z", + "last_active": "2020-01-30T13:34:29.289964Z", + "banned": false, + "online": true, + "image": "https://getstream.io/random_svg/?name=Ancient+salad", + "name": "Ancient salad" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-30T13:36:52.749731Z", + "updated_at": "2020-01-30T13:36:52.749732Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "ancient-salad-0-8c225075-bd4c-42e2-8024-530aae13cd40", + "text": "hi", + "type": "regular", + "user": { + "id": "ancient-salad-0", + "role": "user", + "created_at": "2020-01-30T13:34:29.286813Z", + "updated_at": "2020-01-30T13:34:29.296196Z", + "last_active": "2020-01-30T13:34:29.289964Z", + "banned": false, + "online": true, + "image": "https://getstream.io/random_svg/?name=Ancient+salad", + "name": "Ancient salad" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-30T13:37:41.631056Z", + "updated_at": "2020-01-30T13:37:41.631056Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "proud-sea-7-17802096-cbf8-4e3c-addd-4ee31f4c8b5c", + "text": "😃", + "type": "regular", + "user": { + "id": "proud-sea-7", + "role": "user", + "created_at": "2020-01-30T13:43:03.903006Z", + "updated_at": "2020-01-30T13:43:03.912307Z", + "last_active": "2020-01-30T13:43:03.906236Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Proud+sea", + "name": "Proud sea" + }, + "attachments": [], + "latest_reactions": [], + "own_reactions": [], + "reaction_counts": {}, + "reaction_scores": {}, + "reply_count": 0, + "created_at": "2020-01-30T13:43:41.062362Z", + "updated_at": "2020-01-30T13:43:41.062362Z", + "mentioned_users": [], + "status": "SENT", + "silent": false + } + ], + "watcher_count": 5, + "members": [] + }'''; + + test('should parse json correctly', () { + final channelState = ChannelState.fromJson(json.decode(jsonExample)); + expect(channelState.channel.cid, 'team:dev'); + expect(channelState.channel.id, 'dev'); + expect(channelState.channel.team, 'test'); + expect(channelState.channel.type, 'team'); + expect(channelState.channel.config, isA()); + expect(channelState.channel.config, isNotNull); + expect(channelState.channel.config.commands, hasLength(1)); + expect(channelState.channel.config.commands[0], isA()); + expect(channelState.channel.lastMessageAt, + DateTime.parse("2020-01-30T13:43:41.062362Z")); + expect(channelState.channel.createdAt, + DateTime.parse("2019-04-03T18:43:33.213373Z")); + expect(channelState.channel.updatedAt, + DateTime.parse("2019-04-03T18:43:33.213374Z")); + expect(channelState.channel.createdBy, isA()); + expect(channelState.channel.frozen, true); + expect(channelState.channel.extraData['example'], 1); + expect(channelState.channel.extraData['name'], "#dev"); + expect(channelState.channel.extraData['image'], + "https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png"); + expect(channelState.messages, hasLength(25)); + expect(channelState.messages[0], isA()); + expect(channelState.messages[0], isNotNull); + expect(channelState.messages[0].createdAt, + DateTime.parse("2020-01-29T03:23:02.843948Z")); + expect(channelState.messages[0].user, isA()); + expect(channelState.watcherCount, 5); + }); + + test('should serialize to json correctly', () { + const toJsonExample = r''' + { + "channel": { + "id": "dev", + "type": "team", + "frozen": true, + "name": "#dev", + "image": "https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png", + "example": 1 + }, + "watchers": null, + "read": null, + "messages": [ + { + "id": "dry-meadow-0-2b73cc8b-cd86-4a01-8d40-bd82ad07a030", + "text": "fasdfa", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "dry-meadow-0-e8e74482-b4cd-48db-9d1e-30e6c191786f", + "text": "test message", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "dry-meadow-0-53e6299f-9b97-4a9c-a27e-7e2dde49b7e0", + "text": "test message", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "dry-meadow-0-80925be0-786e-40a5-b225-486518dafd35", + "text": "asdfadf", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "dry-meadow-0-64d7970f-ede8-4b31-9738-1bc1756d2bfe", + "text": "test", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "withered-cell-0-84cbd760-cf55-4f7e-9207-c5f66cccc6dc", + "text": "hi", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "dry-meadow-0-e9203588-43c3-40b1-91f7-f217fc42aa53", + "text": "fantastic", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "withered-cell-0-7e3552d7-7a0d-45f2-a856-e91b23a7e240", + "text": "nice to meet you", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "dry-meadow-0-1ffeafd4-e4fc-4c84-9394-9d7cb10fff42", + "text": "hey", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "dry-meadow-0-3f147324-12c8-4b41-9fb5-2db88d065efa", + "text": "hello, everyone", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "dry-meadow-0-51a348ae-0c0a-44de-a556-eac7891c0cf0", + "text": "who is there?", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "icy-recipe-7-a29e237b-8d81-4a97-9bc8-d42bca3f1356", + "text": "하이", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "icy-recipe-7-935c396e-ddf8-4a9a-951c-0a12fa5bf055", + "text": "what are you doing?", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "throbbing-boat-5-1e4d5730-5ff0-4d25-9948-9f34ffda43e4", + "text": "👍", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "snowy-credit-3-3e0c1a0d-d22f-42ee-b2a1-f9f49477bf21", + "text": "sdasas", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "snowy-credit-3-3319537e-2d0e-4876-8170-a54f046e4b7d", + "text": "cjshsa", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "snowy-credit-3-cfaf0b46-1daa-49c5-947c-b16d6697487d", + "text": "nhisagdhsadz", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "snowy-credit-3-cebe25a7-a3a3-49fc-9919-91c6725e81f3", + "text": "hvadhsahzd", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "divine-glade-9-0cea9262-5766-48e9-8b22-311870aed3bf", + "text": "hello", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "red-firefly-9-c4e9007b-bb7d-4238-ae08-5f8e3cd03d73", + "text": "hello", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "bitter-glade-2-02aee4eb-4093-4736-808b-2de75820e854", + "text": "hello", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "morning-sea-1-0c700bcb-46dd-4224-b590-e77bdbccc480", + "text": "http://jaeger.ui.gtstrm.com/", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "ancient-salad-0-53e8b4e6-5b7b-43ad-aeee-8bfb6a9ed0be", + "text": "hi", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "ancient-salad-0-8c225075-bd4c-42e2-8024-530aae13cd40", + "text": "hi", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + }, + { + "id": "proud-sea-7-17802096-cbf8-4e3c-addd-4ee31f4c8b5c", + "text": "😃", + "attachments": [], + "parent_id": null, + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": null, + "mentioned_users": [], + "status": "SENT", + "silent": false + } + ], + "members": [], + "watcher_count": 5 + } + '''; + final j = jsonDecode(jsonExample); + final channelState = ChannelState( + channel: ChannelModel.fromJson(j['channel']), + members: [], + messages: + (j['messages'] as List).map((m) => Message.fromJson(m)).toList(), + read: null, + watcherCount: 5, + watchers: null, + ); + + expect( + channelState.toJson(), + jsonDecode(toJsonExample), + ); + }); + }); +} diff --git a/packages/dart_client/test/src/models/channel_test.dart b/packages/dart_client/test/src/models/channel_test.dart new file mode 100644 index 00000000..7197d083 --- /dev/null +++ b/packages/dart_client/test/src/models/channel_test.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/src/models/channel_model.dart'; + +void main() { + group('src/models/channel', () { + const jsonExample = ''' + { + "id": "test", + "type": "livestream", + "cid": "test:livestream", + "cats": true, + "fruit": ["bananas", "apples"] + } + '''; + + test('should parse json correctly', () { + final channel = ChannelModel.fromJson(json.decode(jsonExample)); + expect(channel.id, equals("test")); + expect(channel.type, equals("livestream")); + expect(channel.cid, equals("test:livestream")); + expect(channel.extraData["cats"], equals(true)); + expect(channel.extraData["fruit"], equals(["bananas", "apples"])); + }); + + test('should serialize to json correctly', () { + final channel = ChannelModel( + type: "type", + id: "id", + cid: "a:a", + extraData: {"name": "cool"}, + ); + + expect( + channel.toJson(), + {'id': 'id', 'type': 'type', 'name': 'cool'}, + ); + }); + + test('should serialize to json correctly when frozen is provided', () { + final channel = ChannelModel( + type: "type", + id: "id", + cid: "a:a", + extraData: {"name": "cool"}, + frozen: false, + ); + + expect( + channel.toJson(), + {'id': 'id', 'type': 'type', 'name': 'cool', 'frozen': false}, + ); + }); + }); +} diff --git a/packages/dart_client/test/src/models/command_test.dart b/packages/dart_client/test/src/models/command_test.dart new file mode 100644 index 00000000..51774b84 --- /dev/null +++ b/packages/dart_client/test/src/models/command_test.dart @@ -0,0 +1,40 @@ +import 'package:stream_chat/src/models/command.dart'; +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('src/models/command', () { + const jsonExample = ''' + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]" + } + '''; + + test('should parse json correctly', () { + final command = Command.fromJson(json.decode(jsonExample)); + expect(command.name, 'giphy'); + expect(command.description, 'Post a random gif to the channel'); + expect(command.args, '[text]'); + }); + + test('should serialize to json correctly', () { + final command = Command( + name: 'giphy', + description: 'Post a random gif to the channel', + args: '[text]', + ); + + expect( + command.toJson(), + { + "name": "giphy", + "description": "Post a random gif to the channel", + "args": "[text]", + }, + ); + }); + }); +} diff --git a/packages/dart_client/test/src/models/device_test.dart b/packages/dart_client/test/src/models/device_test.dart new file mode 100644 index 00000000..3f68929f --- /dev/null +++ b/packages/dart_client/test/src/models/device_test.dart @@ -0,0 +1,31 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/src/models/device.dart'; + +void main() { + group('src/models/device', () { + const jsonExample = r'''{ + "id": "device-id", + "push_provider": "push-provider" + }'''; + + test('should parse json correctly', () { + final device = Device.fromJson(json.decode(jsonExample)); + expect(device.id, 'device-id'); + expect(device.pushProvider, 'push-provider'); + }); + + test('should serialize to json correctly', () { + final device = Device(id: 'device-id', pushProvider: 'push-provider'); + + expect( + device.toJson(), + { + 'id': 'device-id', + 'push_provider': 'push-provider', + }, + ); + }); + }); +} diff --git a/packages/dart_client/test/src/models/event_test.dart b/packages/dart_client/test/src/models/event_test.dart new file mode 100644 index 00000000..eb7d0f5a --- /dev/null +++ b/packages/dart_client/test/src/models/event_test.dart @@ -0,0 +1,89 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/src/models/event.dart'; +import 'package:stream_chat/src/models/own_user.dart'; +import 'package:stream_chat/stream_chat.dart'; + +void main() { + group('src/models/event', () { + const jsonExample = ''' + { + "type": "type", + "cid": "cid", + "connection_id": "connectionId", + "created_at": "2019-04-03T18:43:33.213374Z", + "me": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + }, + "parent_id": null, + "user": { + "id": "dry-meadow-0", + "role": "user", + "created_at": "2019-03-27T17:40:17.155892Z", + "updated_at": "2020-01-29T03:22:47.641589Z", + "last_active": "2020-01-29T03:22:47.63613Z", + "banned": false, + "online": false, + "image": "https://getstream.io/random_svg/?name=Dry+meadow", + "name": "Dry meadow" + } + } + '''; + + test('should parse json correctly', () { + final event = Event.fromJson(json.decode(jsonExample)); + expect(event.type, 'type'); + expect(event.cid, 'cid'); + expect(event.connectionId, 'connectionId'); + expect(event.createdAt, isA()); + expect(event.me, isA()); + expect(event.user, isA()); + }); + + test('should serialize to json correctly', () { + final event = Event( + user: User(id: 'id'), + type: 'type', + cid: 'cid', + connectionId: 'connectionId', + createdAt: DateTime.parse("2020-01-29T03:22:47.63613Z"), + me: OwnUser(id: 'id2'), + totalUnreadCount: 1, + unreadChannels: 1, + online: true, + ); + + expect( + event.toJson(), + { + 'type': 'type', + 'cid': 'cid', + 'connection_id': 'connectionId', + 'created_at': '2020-01-29T03:22:47.636130Z', + 'me': {'id': 'id2'}, + 'user': {'id': 'id'}, + 'reaction': null, + 'message': null, + 'channel': null, + 'total_unread_count': 1, + 'unread_channels': 1, + 'online': true, + 'is_local': true, + 'member': null, + 'channel_id': null, + 'channel_type': null, + 'parent_id': null, + }, + ); + }); + }); +} diff --git a/packages/dart_client/test/src/models/member_test.dart b/packages/dart_client/test/src/models/member_test.dart new file mode 100644 index 00000000..121b470f --- /dev/null +++ b/packages/dart_client/test/src/models/member_test.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/src/models/member.dart'; +import 'package:stream_chat/src/models/user.dart'; + +void main() { + group('src/models/member', () { + const jsonExample = ''' + { + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + "role": "user", + "created_at": "2020-01-28T22:17:30.826259Z", + "updated_at": "2020-01-28T22:17:31.101222Z", + "banned": false, + "online": false, + "name": "Robin Papa", + "image": "https://pbs.twimg.com/profile_images/669512187778498560/L7wQctBt.jpg" + }, + "role": "member", + "created_at": "2020-01-28T22:17:30.95443Z", + "updated_at": "2020-01-28T22:17:30.95443Z" + } + '''; + + test('should parse json correctly', () { + final member = Member.fromJson(json.decode(jsonExample)); + expect(member.user, isA()); + expect(member.role, 'member'); + expect(member.createdAt, DateTime.parse("2020-01-28T22:17:30.95443Z")); + expect(member.updatedAt, DateTime.parse("2020-01-28T22:17:30.95443Z")); + }); + }); +} diff --git a/packages/dart_client/test/src/models/message_test.dart b/packages/dart_client/test/src/models/message_test.dart new file mode 100644 index 00000000..f318daac --- /dev/null +++ b/packages/dart_client/test/src/models/message_test.dart @@ -0,0 +1,153 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/src/models/attachment.dart'; +import 'package:stream_chat/src/models/message.dart'; +import 'package:stream_chat/src/models/reaction.dart'; +import 'package:stream_chat/src/models/user.dart'; + +void main() { + group('src/models/message', () { + const jsonExample = r'''{ + "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", + "type": "regular", + "silent": false, + "status": "SENT", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "attachments": [ + { + "type": "video", + "author_name": "GIPHY", + "title": "The Lion King Disney GIF - Find \u0026 Share on GIPHY", + "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "text": "Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", + "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA" + } + ], + "latest_reactions": [ + { + "message_id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "user": { + "id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680", + "role": "user", + "created_at": "2020-01-28T22:17:30.83015Z", + "updated_at": "2020-01-28T22:17:31.19435Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/2.jpg", + "name": "Mia Denys" + }, + "type": "love", + "score": 1, + "created_at": "2020-01-28T22:17:31.128376Z", + "updated_at": "2020-01-28T22:17:31.128376Z" + } + ], + "own_reactions": [], + "reaction_counts": { + "love": 1 + }, + "reaction_scores": { + "love": 1 + }, + "reply_count": 0, + "created_at": "2020-01-28T22:17:31.107978Z", + "updated_at": "2020-01-28T22:17:31.130506Z", + "mentioned_users": [] + }'''; + + test('should parse json correctly', () { + final message = Message.fromJson(json.decode(jsonExample)); + expect(message.id, "4637f7e4-a06b-42db-ba5a-8d8270dd926f"); + expect(message.text, + "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA"); + expect(message.type, "regular"); + expect(message.user, isA()); + expect(message.silent, isA()); + expect(message.attachments, isA>()); + expect(message.latestReactions, isA>()); + expect(message.ownReactions, isA>()); + expect(message.reactionCounts, {'love': 1}); + expect(message.reactionScores, {'love': 1}); + expect(message.createdAt, DateTime.parse("2020-01-28T22:17:31.107978Z")); + expect(message.updatedAt, DateTime.parse("2020-01-28T22:17:31.130506Z")); + expect(message.mentionedUsers, isA>()); + }); + + test('should serialize to json correctly', () { + final message = Message( + id: "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + text: + "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", + silent: false, + attachments: [ + Attachment.fromJson({ + "type": "video", + "author_name": "GIPHY", + "title": "The Lion King Disney GIF - Find \u0026 Share on GIPHY", + "title_link": + "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "text": + "Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "image_url": + "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "thumb_url": + "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "asset_url": + "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4", + "og_scrape_url": + "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA" + }) + ], + showInChannel: true, + parentId: 'parentId', + extraData: {'hey': 'test'}, + status: MessageSendingStatus.SENT, + ); + + expect( + message.toJson(), + json.decode(r''' + { + "id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f", + "text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", + "silent": false, + "attachments": [ + { + "type": "video", + "title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "title": "The Lion King Disney GIF - Find & Share on GIPHY", + "thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "text": "Discover & share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.", + "og_scrape_url": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA", + "image_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif", + "author_name": "GIPHY", + "asset_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4" + } + ], + "mentioned_users": null, + "parent_id": "parentId", + "quoted_message": null, + "quoted_message_id": null, + "show_in_channel": true, + "hey": "test" + } + '''), + ); + }); + }); +} diff --git a/packages/dart_client/test/src/models/reaction_test.dart b/packages/dart_client/test/src/models/reaction_test.dart new file mode 100644 index 00000000..2e8fe5f8 --- /dev/null +++ b/packages/dart_client/test/src/models/reaction_test.dart @@ -0,0 +1,72 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/src/models/reaction.dart'; +import 'dart:convert'; + +import 'package:stream_chat/src/models/user.dart'; + +void main() { + group('src/models/reaction', () { + const jsonExample = ''' + { + "message_id": "76cd8c82-b557-4e48-9d12-87995d3a0e04", + "user_id": "2de0297c-f3f2-489d-b930-ef77342edccf", + "user": { + "id": "2de0297c-f3f2-489d-b930-ef77342edccf", + "role": "user", + "created_at": "2020-01-28T22:17:30.810011Z", + "updated_at": "2020-01-28T22:17:31.077195Z", + "banned": false, + "online": false, + "image": "https://randomuser.me/api/portraits/women/45.jpg", + "name": "Daisy Morgan" + }, + "type": "wow", + "score": 1, + "created_at": "2020-01-28T22:17:31.108742Z", + "updated_at": "2020-01-28T22:17:31.108742Z" + } + '''; + + test('should parse json correctly', () { + final reaction = Reaction.fromJson(json.decode(jsonExample)); + expect(reaction.messageId, '76cd8c82-b557-4e48-9d12-87995d3a0e04'); + expect(reaction.createdAt, DateTime.parse("2020-01-28T22:17:31.108742Z")); + expect(reaction.type, 'wow'); + expect( + reaction.user.toJson(), + User.init("2de0297c-f3f2-489d-b930-ef77342edccf", extraData: { + "image": "https://randomuser.me/api/portraits/women/45.jpg", + "name": "Daisy Morgan" + }).toJson(), + ); + expect(reaction.score, 1); + expect(reaction.userId, '2de0297c-f3f2-489d-b930-ef77342edccf'); + expect(reaction.extraData, {'updated_at': '2020-01-28T22:17:31.108742Z'}); + }); + + test('should serialize to json correctly', () { + final reaction = Reaction( + messageId: '76cd8c82-b557-4e48-9d12-87995d3a0e04', + createdAt: DateTime.parse("2020-01-28T22:17:31.108742Z"), + type: 'wow', + user: User.init("2de0297c-f3f2-489d-b930-ef77342edccf", extraData: { + "image": "https://randomuser.me/api/portraits/women/45.jpg", + "name": "Daisy Morgan" + }), + userId: "2de0297c-f3f2-489d-b930-ef77342edccf", + extraData: {'bananas': 'yes'}, + score: 1, + ); + + expect( + reaction.toJson(), + { + "message_id": "76cd8c82-b557-4e48-9d12-87995d3a0e04", + "type": "wow", + "score": 1, + "bananas": 'yes', + }, + ); + }); + }); +} diff --git a/packages/dart_client/test/src/models/read_test.dart b/packages/dart_client/test/src/models/read_test.dart new file mode 100644 index 00000000..83596170 --- /dev/null +++ b/packages/dart_client/test/src/models/read_test.dart @@ -0,0 +1,40 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/src/models/read.dart'; +import 'package:stream_chat/src/models/user.dart'; + +void main() { + group('src/models/read', () { + const jsonExample = ''' + { + "user": { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e" + }, + "last_read": "2020-01-28T22:17:30.966485504Z", + "unread_messages": 10 + } + '''; + + test('should parse json correctly', () { + final read = Read.fromJson(json.decode(jsonExample)); + expect(read.lastRead, DateTime.parse('2020-01-28T22:17:30.966485504Z')); + expect(read.user.id, 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e'); + expect(read.unreadMessages, 10); + }); + + test('should serialize to json correctly', () { + final read = Read( + lastRead: DateTime.parse('2020-01-28T22:17:30.966485504Z'), + user: User.init('bbb19d9a-ee50-45bc-84e5-0584e79d0c9e'), + unreadMessages: 10, + ); + + expect(read.toJson(), { + "user": {"id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e"}, + "last_read": "2020-01-28T22:17:30.966485Z", + 'unread_messages': 10, + }); + }); + }); +} diff --git a/packages/dart_client/test/src/models/serialization_test.dart b/packages/dart_client/test/src/models/serialization_test.dart new file mode 100644 index 00000000..c20d0012 --- /dev/null +++ b/packages/dart_client/test/src/models/serialization_test.dart @@ -0,0 +1,60 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/src/models/serialization.dart'; + +void main() { + group('src/models/serialization', () { + test('should move unknown keys from root to dedicate property', () { + final json = { + 'prop1': 'test', + 'prop2': 123, + 'prop3': true, + }; + final result = Serialization.moveToExtraDataFromRoot(json, [ + 'prop1', + 'prop2', + ]); + + expect(result, { + 'prop1': 'test', + 'prop2': 123, + 'extra_data': { + 'prop3': true, + }, + }); + + expect(json, { + 'prop1': 'test', + 'prop2': 123, + 'prop3': true, + }); + }); + + test('should have empty extraData', () { + final result = Serialization.moveToExtraDataFromRoot({ + 'prop1': 'test', + 'prop2': 123, + 'prop3': true, + }, [ + 'prop1', + 'prop2', + 'prop3' + ]); + + expect(result, { + 'prop1': 'test', + 'prop2': 123, + 'prop3': true, + 'extra_data': {}, + }); + }); + + test('should return null', () { + final result = Serialization.moveToExtraDataFromRoot(null, [ + 'prop1', + 'prop2', + ]); + + expect(result, null); + }); + }); +} diff --git a/packages/dart_client/test/src/models/user_test.dart b/packages/dart_client/test/src/models/user_test.dart new file mode 100644 index 00000000..a148ec6e --- /dev/null +++ b/packages/dart_client/test/src/models/user_test.dart @@ -0,0 +1,28 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/src/models/user.dart'; + +void main() { + group('src/models/user', () { + const jsonExample = ''' + { + "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e" + } + '''; + + test('should parse json correctly', () { + final user = User.fromJson(json.decode(jsonExample)); + expect(user.id, 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e'); + }); + + test('should serialize to json correctly', () { + final user = + User(id: 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e', role: "abc"); + + expect(user.toJson(), { + 'id': "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", + }); + }); + }); +} diff --git a/packages/dart_client/test/version_test.dart b/packages/dart_client/test/version_test.dart new file mode 100644 index 00000000..698f79e1 --- /dev/null +++ b/packages/dart_client/test/version_test.dart @@ -0,0 +1,23 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/version.dart'; + +void prepareTest() { + // https://github.com/flutter/flutter/issues/20907 + if (Directory.current.path.endsWith('/test')) { + Directory.current = Directory.current.parent; + } +} + +void main() { + prepareTest(); + test('stream chat version matches pubspec', () { + final String pubspecPath = '${Directory.current.path}/pubspec.yaml'; + final String pubspec = File(pubspecPath).readAsStringSync(); + final RegExp regex = RegExp('version:\s*(.*)'); + final RegExpMatch match = regex.firstMatch(pubspec); + expect(match, isNotNull); + expect(PACKAGE_VERSION, match.group(1).trim()); + }); +} diff --git a/packages/dart_client/test_integration/client_test.dart b/packages/dart_client/test_integration/client_test.dart new file mode 100644 index 00000000..98ec39cf --- /dev/null +++ b/packages/dart_client/test_integration/client_test.dart @@ -0,0 +1,21 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:logging/logging.dart'; +import 'package:stream_chat/src/client.dart'; +import 'package:stream_chat/stream_chat.dart'; + +const API_KEY = '6xjf3dex3n7d'; +const TOKEN = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoid2lsZC1icmVlemUtNyJ9.VM2EX1EXOfgqa-bTH_3JzeY0T99ngWzWahSauP3dBMo'; + +void main() { + test('test', () async { + final client = Client( + '6xjf3dex3n7d', + logLevel: Level.INFO, + tokenProvider: (_) async => '', + ); + final user = User(id: 'wild-breeze-7'); + + await client.setGuestUser(user); + }); +}