diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..58b65f9
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,33 @@
+# This is a basic workflow to help you get started with Actions
+
+name: Publish to Pub.dev
+
+# Controls when the workflow will run
+on:
+ ## Triggers the workflow on push or pull request events but only for the main branch
+ #push:
+ # branches: [ main ]
+ #pull_request:
+ # branches: [ main ]
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+# A workflow run is made up of one or more jobs that can run sequentially or in parallel
+jobs:
+ publishing:
+ # The type of runner that the job will run on
+ runs-on: ubuntu-latest
+
+ # Steps represent a sequence of tasks that will be executed as part of the job
+ steps:
+ # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
+ - name: 'Checkout'
+ uses: actions/checkout@v2 # required!
+
+ - name: '>> Dart package <<'
+ uses: k-paxian/dart-package-publisher@master
+ with:
+ accessToken: ${{ secrets.OAUTH_ACCESS_TOKEN }}
+ refreshToken: ${{ secrets.OAUTH_REFRESH_TOKEN }}
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1985397
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,74 @@
+# 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/
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Flutter.podspec
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
diff --git a/.metadata b/.metadata
new file mode 100644
index 0000000..54f6c7b
--- /dev/null
+++ b/.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: 1d9032c7e1d867f071f2277eb1673e8f9b0274e3
+ channel: stable
+
+project_type: package
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..8ae19cf
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,4 @@
+## 0.1.0
+
+- Initial version.
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e67741e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (c) 2022, TalkJS
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
index 77f41e6..85c09e8 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,64 @@
-# flutter-sdk-victor
\ No newline at end of file
+# TalkJS Flutter SDK
+
+Official TalkJS SDK for Flutter
+
+**What is TalkJS?**
+
+TalkJS lets you add user-to-user chat to your marketplace, on-demand app, or
+social platform.
+For more information, visit
+[talkjs.com](https://talkjs.com/?ref=jssdk-npm-readme).
+
+
+
+Don't hesitate to
+[let us know](https://talkjs.com/?chat)
+if you have any questions about TalkJS.
+
+## Requirements
+
+- Dart sdk: ">=2.15.0 <3.0.0"
+- Flutter: ">=2.8.1"
+- Android: `minSDKVersion 19`
+
+## Installation
+
+Edit the dependencies section of your project's `pubspec.yaml` file in your
+Flutter project as follows:
+
+```yaml
+dependencies:
+ talkjs_flutter: ^0.1.0
+```
+
+Run the command: `flutter pub get` on the command line or through Android
+Studio's **Get dependencies** button.
+
+
+## Usage
+
+Import TalkJS in your project source files.
+
+```dart
+import 'package:talkjs_flutter/talkjs.dart';
+```
+
+Then follow our
+[Flutter guide](https://talkjs.com/docs/Getting_Started/Frameworks/Flutter/)
+to start using TalkJS in your project.
+
+## TalkJS is fully forward compatible
+We promise to never break API compatibility.
+We may at times deprecate methods or fields, but we will never remove them.
+If something that used to work stops working, then that's a bug.
+Please [report it](https://talkjs.com/?chat) and we'll fix it asap.
+
+The package is being released in a beta state.
+The reason for this is that there are things that one can do with the TalkJS
+JavaScript SDK that aren't possible with the Flutter SDK.
+We will release v1.0.0 of this package once the two SDKs are similar in terms
+of features.
+This however does not take away from our commitment to always maintain backward
+compatibility.
+So you can be assured that the package is stable for production use.
+
diff --git a/lib/assets/index.html b/lib/assets/index.html
new file mode 100644
index 0000000..d9de508
--- /dev/null
+++ b/lib/assets/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/src/chatbox.dart b/lib/src/chatbox.dart
new file mode 100644
index 0000000..a2b7c06
--- /dev/null
+++ b/lib/src/chatbox.dart
@@ -0,0 +1,473 @@
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/foundation.dart';
+
+import 'package:webview_flutter/webview_flutter.dart';
+
+import './session.dart';
+import './conversation.dart';
+import './chatoptions.dart';
+import './user.dart';
+import './message.dart';
+import './predicate.dart';
+
+typedef SendMessageHandler = void Function(SendMessageEvent event);
+typedef TranslationToggledHandler = void Function(TranslationToggledEvent event);
+
+class SendMessageEvent {
+ final ConversationData conversation;
+ final UserData me;
+ final SentMessage message;
+
+ SendMessageEvent.fromJson(Map json)
+ : conversation = ConversationData.fromJson(json['conversation']),
+ me = UserData.fromJson(json['me']),
+ message = SentMessage.fromJson(json['message']);
+}
+
+class TranslationToggledEvent {
+ final ConversationData conversation;
+ final bool isEnabled;
+
+ TranslationToggledEvent.fromJson(Map json)
+ : conversation = ConversationData.fromJson(json['conversation']),
+ isEnabled = json['isEnabled'];
+}
+
+/// A messaging UI for just a single conversation.
+///
+/// Create a Chatbox through [Session.createChatbox] and then call [mount] to show it.
+/// There is no way for the user to switch between conversations
+class ChatBox extends StatefulWidget {
+ final Session session;
+
+ final TextDirection? dir;
+ final MessageFieldOptions? messageField;
+ final bool? showChatHeader;
+ final TranslationToggle? showTranslationToggle;
+ final String? theme;
+ final TranslateConversations? translateConversations;
+ final List highlightedWords = const [];
+ final MessagePredicate messageFilter;
+
+ final Conversation? conversation;
+ final bool? asGuest;
+
+ final SendMessageHandler? onSendMessage;
+ final TranslationToggledHandler? onTranslationToggled;
+
+ const ChatBox({
+ Key? key,
+ required this.session,
+ this.dir,
+ this.messageField,
+ this.showChatHeader,
+ this.showTranslationToggle,
+ this.theme,
+ this.translateConversations,
+ //this.highlightedWords = const [], // Commented out due to bug #1953
+ this.messageFilter = const MessagePredicate(),
+ this.conversation,
+ this.asGuest,
+ this.onSendMessage,
+ this.onTranslationToggled,
+ }) : super(key: key);
+
+ @override
+ State createState() => ChatBoxState();
+}
+
+class ChatBoxState extends State {
+ /// Used to control the underlying WebView
+ WebViewController? _webViewController;
+ bool _webViewCreated = false;
+
+ /// List of JavaScript statements that haven't been executed.
+ final _pending = [];
+
+ // A counter to ensure that IDs are unique
+ int _idCounter = 0;
+
+ /// A mapping of user ids to the variable name of the respective JavaScript
+ /// Talk.User object.
+ final _users = {};
+ final _userObjs = {};
+
+ /// A mapping of conversation ids to the variable name of the respective JavaScript
+ /// Talk.ConversationBuilder object.
+ final _conversations = {};
+ final _conversationObjs = {};
+
+ /// Encapsulates the message entry field tied to the currently selected conversation.
+ // TODO: messageField still needs to be refactored
+ //late MessageField messageField;
+
+ /// Objects stored for comparing changes
+ ChatBoxOptions? _oldOptions;
+ List _oldHighlightedWords = [];
+ MessagePredicate _oldMessageFilter = const MessagePredicate();
+ bool? _oldAsGuest;
+ Conversation? _oldConversation;
+
+ @override
+ Widget build(BuildContext context) {
+ if (kDebugMode) {
+ print('📗 chatbox.build (_webViewCreated: $_webViewCreated)');
+ }
+
+ if (!_webViewCreated) {
+ // If it's the first time that the widget is built, then build everything
+ _webViewCreated = true;
+
+ execute('let chatBox;');
+
+ _createSession();
+ _createChatBox();
+ // messageFilter and highlightedWords are set as options for the chatbox
+ _createConversation();
+
+ execute('chatBox.mount(document.getElementById("talkjs-container"));');
+ } else {
+ // If it's not the first time that the widget is built,
+ // then check what needs to be rebuilt
+
+ // TODO: If something has changed in the Session we should do something
+
+ final chatBoxRecreated = _checkRecreateChatBox();
+
+ if (chatBoxRecreated) {
+ // messageFilter and highlightedWords are set as options for the chatbox
+ _createConversation();
+ } else {
+ _checkMessageFilter();
+ _checkHighlightedWords();
+ _checkRecreateConversation();
+ }
+
+ // Mount the chatbox only if it's new (else the existing chatbox has already been mounted)
+ if (chatBoxRecreated) {
+ execute('chatBox.mount(document.getElementById("talkjs-container"));');
+ }
+ }
+
+ return WebView(
+ initialUrl: 'about:blank',
+ javascriptMode: JavascriptMode.unrestricted,
+ debuggingEnabled: kDebugMode,
+ onWebViewCreated: _webViewCreatedCallback,
+ onPageFinished: _onPageFinished,
+ javascriptChannels: {
+ JavascriptChannel(name: 'JSCSendMessage', onMessageReceived: _jscSendMessage),
+ JavascriptChannel(name: 'JSCTranslationToggled', onMessageReceived: _jscTranslationToggled),
+ });
+ }
+
+ void _createSession() {
+ // Initialize Session object
+ final options = {};
+
+ options['appId'] = widget.session.appId;
+
+ if (widget.session.signature != null) {
+ options["signature"] = widget.session.signature;
+ }
+
+ execute('const options = ${json.encode(options)};');
+
+ final variableName = getUserVariableName(widget.session.me);
+ execute('options["me"] = $variableName;');
+
+ execute('const session = new Talk.Session(options);');
+ }
+
+ void _createChatBox() {
+ _oldOptions = ChatBoxOptions(
+ dir: widget.dir,
+ messageField: widget.messageField,
+ showChatHeader: widget.showChatHeader,
+ showTranslationToggle: widget.showTranslationToggle,
+ theme: widget.theme,
+ translateConversations: widget.translateConversations,
+ );
+
+ _oldHighlightedWords = List.of(widget.highlightedWords);
+ _oldMessageFilter = MessagePredicate.of(widget.messageFilter);
+
+ execute('chatBox = session.createChatbox(${_oldOptions!.getJsonString(this)});');
+
+ execute('chatBox.on("sendMessage", (event) => JSCSendMessage.postMessage(JSON.stringify(event)));');
+ execute('chatBox.on("translationToggled", (event) => JSCTranslationToggled.postMessage(JSON.stringify(event)));');
+ }
+
+ bool _checkRecreateChatBox() {
+ final options = ChatBoxOptions(
+ dir: widget.dir,
+ messageField: widget.messageField,
+ showChatHeader: widget.showChatHeader,
+ showTranslationToggle: widget.showTranslationToggle,
+ theme: widget.theme,
+ translateConversations: widget.translateConversations,
+ );
+
+ if (options != _oldOptions) {
+ execute('chatBox.destroy();');
+ _createChatBox();
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ void _createConversation() {
+ final result = {};
+
+ _oldAsGuest = widget.asGuest;
+ if (_oldAsGuest != null) {
+ result['asGuest'] = _oldAsGuest;
+ }
+
+ _oldConversation = widget.conversation;
+ if (_oldConversation != null) {
+ execute('chatBox.select(${getConversationVariableName(_oldConversation!)}, ${json.encode(result)});');
+ } else {
+ if (result.isNotEmpty) {
+ execute('chatBox.select(undefined, ${json.encode(result)});');
+ } else {
+ execute('chatBox.select(undefined);');
+ }
+ }
+ }
+
+ bool _checkRecreateConversation() {
+ if ((widget.asGuest != _oldAsGuest) || (widget.conversation != _oldConversation)) {
+ _createConversation();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ void _setHighlightedWords() {
+ _oldHighlightedWords = List.of(widget.highlightedWords);
+
+ execute('chatBox.setHighlightedWords(${json.encode(_oldHighlightedWords)});');
+ }
+
+ bool _checkHighlightedWords() {
+ if (!listEquals(widget.highlightedWords, _oldHighlightedWords)) {
+ _setHighlightedWords();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ void _setMessageFilter() {
+ _oldMessageFilter = MessagePredicate.of(widget.messageFilter);
+
+ execute('chatBox.setMessageFilter(${json.encode(_oldMessageFilter)});');
+ }
+
+ bool _checkMessageFilter() {
+ if (widget.messageFilter != _oldMessageFilter) {
+ _setMessageFilter();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ void _webViewCreatedCallback(WebViewController webViewController) async {
+ if (kDebugMode) {
+ print('📗 chatbox._webViewCreatedCallback');
+ }
+
+ String htmlData = await rootBundle.loadString('packages/talkjs_flutter/assets/index.html');
+ Uri uri = Uri.dataFromString(htmlData, mimeType: 'text/html', encoding: Encoding.getByName('utf-8'));
+ webViewController.loadUrl(uri.toString());
+
+ _webViewController = webViewController;
+ }
+
+ void _onPageFinished(String url) {
+ if (kDebugMode) {
+ print('📗 chatbox._onPageFinished');
+ }
+
+ if (url != 'about:blank') {
+ // Wait for TalkJS to be ready
+ // Not all WebViews support top level await, so it's better to use an
+ // async IIFE
+ final js = '(async function () { await Talk.ready; }());';
+
+ if (kDebugMode) {
+ print('📗 chatbox._onPageFinished: $js');
+ }
+
+ _webViewController!.runJavascript(js);
+
+ // Execute any pending instructions
+ for (var statement in _pending) {
+ if (kDebugMode) {
+ print('📗 chatbox._onPageFinished _pending: $statement');
+ }
+
+ _webViewController!.runJavascript(statement);
+ }
+ }
+ }
+
+ void _jscSendMessage(JavascriptMessage message) {
+ if (kDebugMode) {
+ print('📗 chatbox._jscSendMessage: ${message.message}');
+ }
+
+ widget.onSendMessage?.call(SendMessageEvent.fromJson(json.decode(message.message)));
+ }
+
+ void _jscTranslationToggled(JavascriptMessage message) {
+ if (kDebugMode) {
+ print('📗 chatbox._jscTranslationToggled: ${message.message}');
+ }
+
+ widget.onTranslationToggled?.call(TranslationToggledEvent.fromJson(json.decode(message.message)));
+ }
+
+ /// For internal use only. Implementation detail that may change anytime.
+ ///
+ /// Return a string with a unique ID
+ String getUniqueId() {
+ final id = _idCounter;
+
+ _idCounter += 1;
+
+ return '_$id';
+ }
+
+ /// For internal use only. Implementation detail that may change anytime.
+ ///
+ /// Returns the JavaScript variable name of the Talk.User object associated
+ /// with the given [User]
+ String getUserVariableName(User user) {
+ if (_users[user.id] == null) {
+ // Generate unique variable name
+ final variableName = 'user${getUniqueId()}';
+
+ _users[user.id] = variableName;
+
+ execute('let $variableName = new Talk.User(${user.getJsonString()});');
+
+ _userObjs[user.id] = User.of(user);
+ } else if (_userObjs[user.id] != user) {
+ final variableName = _users[user.id]!;
+
+ execute('$variableName = new Talk.User(${user.getJsonString()});');
+
+ _userObjs[user.id] = User.of(user);
+ }
+
+ return _users[user.id]!;
+ }
+
+ /// For internal use only. Implementation detail that may change anytime.
+ String getConversationVariableName(Conversation conversation) {
+ if (_conversations[conversation.id] == null) {
+ final variableName = 'conversation${getUniqueId()}';
+
+ _conversations[conversation.id] = variableName;
+
+ execute('let $variableName = session.getOrCreateConversation("${conversation.id}")');
+
+ _setConversationAttributes(variableName, conversation);
+ _setConversationParticipants(variableName, conversation);
+
+ _conversationObjs[conversation.id] = Conversation.of(conversation);
+ } else if (_conversationObjs[conversation.id] != conversation) {
+ final variableName = _conversations[conversation.id]!;
+
+ _setConversationAttributes(variableName, conversation);
+
+ if (!setEquals(conversation.participants, _conversationObjs[conversation.id]!.participants)) {
+ _setConversationParticipants(variableName, conversation);
+ }
+
+ _conversationObjs[conversation.id] = Conversation.of(conversation);
+ }
+
+ return _conversations[conversation.id]!;
+ }
+
+ void _setConversationAttributes(String variableName, Conversation conversation) {
+ final attributes = {};
+
+ if (conversation.custom != null) {
+ attributes['custom'] = conversation.custom;
+ }
+
+ if (conversation.welcomeMessages != null) {
+ attributes['welcomeMessages'] = conversation.welcomeMessages;
+ }
+
+ if (conversation.photoUrl != null) {
+ attributes['photoUrl'] = conversation.photoUrl;
+ }
+
+ if (conversation.subject != null) {
+ attributes['subject'] = conversation.subject;
+ }
+
+ if (attributes.isNotEmpty) {
+ execute('$variableName.setAttributes(${json.encode(attributes)});');
+ }
+ }
+
+ void _setConversationParticipants(String variableName, Conversation conversation) {
+ for (var participant in conversation.participants) {
+ final userVariableName = getUserVariableName(participant.user);
+ final result = {};
+
+ if (participant.access != null) {
+ result['access'] = participant.access!.getValue();
+ }
+
+ if (participant.notify != null) {
+ result['notify'] = participant.notify;
+ }
+
+ execute('$variableName.setParticipant($userVariableName, ${json.encode(result)});');
+ }
+ }
+
+ /// For internal use only. Implementation detail that may change anytime.
+ ///
+ /// Sets the options for ChatBoxOptions for the properties where there exists
+ /// both a declarative option and an imperative method
+ void setExtraOptions(Map result) {
+ result['highlightedWords'] = widget.highlightedWords;
+ result['messageFilter'] = widget.messageFilter;
+ }
+
+ /// For internal use only. Implementation detail that may change anytime.
+ ///
+ /// Evaluates the JavaScript statement given.
+ void execute(String statement) {
+ final controller = _webViewController;
+
+ if (kDebugMode) {
+ print('📘 chatbox.execute: $statement');
+ }
+
+ if (controller != null) {
+ controller.runJavascript(statement);
+ } else {
+ this._pending.add(statement);
+ }
+ }
+}
+
diff --git a/lib/src/chatoptions.dart b/lib/src/chatoptions.dart
new file mode 100644
index 0000000..7a0c9ea
--- /dev/null
+++ b/lib/src/chatoptions.dart
@@ -0,0 +1,265 @@
+import 'dart:convert';
+import 'dart:ui';
+
+import './chatbox.dart';
+
+/// The values that dictate the chat direction.
+enum TextDirection {
+ /// right-to-left
+ rtl,
+ /// left-to-right
+ ltr,
+}
+
+
+/// Settings that affect the behavior of the message field
+class MessageFieldOptions {
+ /// Determines whether the message field should automatically focus when the
+ /// user navigates.
+ ///
+ /// Defaults to "smart", which means that the message field gets focused
+ /// whenever a conversation is selected, if possible without negative side
+ /// effects.
+ /// If you need more control, consider setting [autofocus] to false and
+ /// calling focus() at appropriate times.
+ final bool? autofocus; // Convert to "smart"
+
+ /// If set to true, pressing the enter key sends the message
+ /// (if there is text in the message field).
+ ///
+ /// When set to false, the only way to send a message is by clicking or
+ /// touching the "Send" button.
+ /// Defaults to true.
+ final bool? enterSendsMessage;
+
+ /// The text displayed in the message field when the user hasn't started
+ /// typing anything.
+ final String? placeholder;
+
+ /// This enables spell checking.
+ ///
+ /// Note that setting this to true may also enable autocorrect on some mobile
+ /// devices.
+ /// Defaults to false
+ final bool? spellcheck;
+
+ /// TODO: visible
+
+ const MessageFieldOptions({this.autofocus, this.enterSendsMessage, this.placeholder, this.spellcheck});
+
+ Map toJson() {
+ final result = {};
+
+ if (autofocus != null) {
+ if (autofocus == true) {
+ result['autofocus'] = 'smart';
+ } else {
+ result['autofocus'] = autofocus;
+ }
+ }
+
+ if (autofocus != null) {
+ result['enterSendsMessage'] = enterSendsMessage;
+ }
+
+ if (autofocus != null) {
+ result['placeholder'] = placeholder;
+ }
+
+ if (autofocus != null) {
+ result['spellcheck'] = spellcheck;
+ }
+
+ return result;
+ }
+
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+
+ if (!(other is MessageFieldOptions)) {
+ return false;
+ }
+
+ if (autofocus != other.autofocus) {
+ return false;
+ }
+
+ if (enterSendsMessage != other.enterSendsMessage) {
+ return false;
+ }
+
+ if (placeholder != other.placeholder) {
+ return false;
+ }
+
+ if (spellcheck != other.spellcheck) {
+ return false;
+ }
+
+ return true;
+ }
+
+ int get hashCode => hashValues(autofocus, enterSendsMessage, placeholder, spellcheck);
+}
+
+/// The possible values for showTranslationToggle
+enum TranslationToggle { off, on, auto }
+
+extension TranslationToggleValue on TranslationToggle {
+ /// Converts this enum's values to String.
+ dynamic getValue() {
+ switch (this) {
+ case TranslationToggle.off:
+ return false;
+ case TranslationToggle.on:
+ return true;
+ case TranslationToggle.auto:
+ return 'auto';
+ }
+ }
+}
+
+/// The possible values for translateConversations
+enum TranslateConversations { off, on, auto }
+
+extension TranslateConversationsValue on TranslateConversations {
+ /// Converts this enum's values to String.
+ dynamic getValue() {
+ switch (this) {
+ case TranslateConversations.off:
+ return false;
+ case TranslateConversations.on:
+ return true;
+ case TranslateConversations.auto:
+ return 'auto';
+ }
+ }
+}
+
+/// Options to configure the behaviour of the [ChatBox] UI.
+class ChatBoxOptions {
+ /// Controls the text direction (for supporting right-to-left languages such
+ /// as Arabic and Hebrew).
+ ///
+ /// Defaults to [TextDirection.rtl].
+ final TextDirection? dir;
+
+ /// Settings that affect the behavior of the message field
+ final MessageFieldOptions? messageField;
+
+ /// TODO: messageFilter
+
+ /// Used to control if the Chat Header is displayed in the UI.
+ ///
+ /// Defaults to true.
+ final bool? showChatHeader;
+
+ /// Set this to on to show a translation toggle in all conversations.
+ /// Set this to auto to show a translation toggle in conversations where there are participants with different locales.
+ final TranslationToggle? showTranslationToggle;
+
+ /// Overrides the theme used for this chat UI.
+ final String? theme;
+
+ /// TODO: thirdparties
+
+ /// Enables conversation translation with Google Translate.
+ final TranslateConversations? translateConversations;
+
+ const ChatBoxOptions({
+ this.dir,
+ this.messageField,
+ this.showChatHeader,
+ this.showTranslationToggle,
+ this.theme,
+ this.translateConversations,
+ });
+
+ /// For internal use only. Implementation detail that may change anytime.
+ ///
+ /// This method is used instead of toJson, as we need to output valid JS
+ /// that is not valid JSON.
+ /// The toJson method is intentionally omitted, to produce an error if
+ /// someone tries to convert this object to JSON instead of using the
+ /// getJsonString method.
+ String getJsonString(ChatBoxState chatBox) {
+ final result = {};
+
+ if (dir != null) {
+ result['dir'] = dir!.name;
+ }
+
+ if (messageField != null) {
+ result['messageField'] = messageField;
+ }
+
+ if (showChatHeader != null) {
+ result['showChatHeader'] = showChatHeader;
+ }
+
+ // 'auto' gets the priority over the boolean value
+ if (showTranslationToggle != null) {
+ result['showTranslationToggle'] = showTranslationToggle!.getValue();
+ }
+
+ if (theme != null) {
+ result['theme'] = theme;
+ }
+
+ if (translateConversations != null) {
+ result['translateConversations'] = translateConversations!.getValue();
+ }
+
+ chatBox.setExtraOptions(result);
+
+ return json.encode(result);
+ }
+
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+
+ if (!(other is ChatBoxOptions)) {
+ return false;
+ }
+
+ if (dir != other.dir) {
+ return false;
+ }
+
+ if (messageField != other.messageField) {
+ return false;
+ }
+
+ if (showChatHeader != other.showChatHeader) {
+ return false;
+ }
+
+ if (showTranslationToggle != other.showTranslationToggle) {
+ return false;
+ }
+
+ if (theme != other.theme) {
+ return false;
+ }
+
+ if (translateConversations != other.translateConversations) {
+ return false;
+ }
+
+ return true;
+ }
+
+ int get hashCode => hashValues(
+ dir,
+ messageField,
+ showChatHeader,
+ showTranslationToggle,
+ theme,
+ translateConversations,
+ );
+}
+
diff --git a/lib/src/conversation.dart b/lib/src/conversation.dart
new file mode 100644
index 0000000..39787df
--- /dev/null
+++ b/lib/src/conversation.dart
@@ -0,0 +1,207 @@
+import 'dart:ui';
+
+import 'package:flutter/foundation.dart';
+
+import './session.dart';
+import './user.dart';
+
+/// Possible values for participants' permissions
+enum ParticipantAccess { read, readWrite }
+
+extension ParticipantAccessString on ParticipantAccess {
+ /// Converts this enum's values to String.
+ String getValue() {
+ switch (this) {
+ case ParticipantAccess.read:
+ return 'Read';
+ case ParticipantAccess.readWrite:
+ return 'ReadWrite';
+ }
+ }
+}
+
+// Participants are users + options relative to this conversation
+class Participant {
+ final User user;
+
+ final ParticipantAccess? access;
+
+ final bool? notify;
+
+ const Participant(this.user, {this.access, this.notify});
+
+ Participant.of(Participant other)
+ : user = User.of(other.user),
+ access = other.access,
+ notify = other.notify;
+
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+
+ if (!(other is Participant)) {
+ return false;
+ }
+
+ if (user != other.user) {
+ return false;
+ }
+
+ if (access != other.access) {
+ return false;
+ }
+
+ if (notify != other.notify) {
+ return false;
+ }
+
+ return true;
+ }
+
+ int get hashCode => hashValues(user, access, notify);
+}
+
+/// This represents a conversation that is about to be created, fetched, or
+/// updated.
+///
+/// You can use this object to set up or modify a conversation before showing it.
+/// Note: any changes you make here will not be sent to TalkJS immediately.
+/// Instead, instantiate a TalkJS UI using methods such as [Session.createInbox].
+class _BaseConversation {
+ /// The unique conversation identifier.
+ final String id;
+
+ /// Custom metadata for this conversation
+ final Map? custom;
+
+ /// Messages sent at the beginning of a chat.
+ ///
+ /// The messages will appear as system messages.
+ final List? welcomeMessages;
+
+ /// The URL to a photo which will be shown as the photo for the conversation.
+ final String? photoUrl;
+
+ /// The conversation subject which will be displayed in the chat header.
+ final String? subject;
+
+ const _BaseConversation({
+ required this.id,
+ this.custom,
+ this.welcomeMessages,
+ this.photoUrl,
+ this.subject,
+ });
+}
+
+class Conversation extends _BaseConversation {
+ // The participants for this conversation
+ final Set participants;
+
+ // To tie the conversation to a session
+ final Session _session;
+
+ const Conversation({
+ required Session session,
+ required String id,
+ Map? custom,
+ List? welcomeMessages,
+ String? photoUrl,
+ String? subject,
+ required this.participants,
+ })
+ : _session = session,
+ super(
+ id: id,
+ custom: custom,
+ welcomeMessages: welcomeMessages,
+ photoUrl: photoUrl,
+ subject: subject,
+ );
+
+ Conversation.of(Conversation other)
+ : _session = other._session,
+ participants = Set.of(other.participants.map((participant) => Participant.of(participant))),
+ super(
+ id: other.id,
+ custom: (other.custom != null ? Map.of(other.custom!) : null),
+ welcomeMessages: (other.welcomeMessages != null ? List.of(other.welcomeMessages!) : null),
+ photoUrl: other.photoUrl,
+ subject: other.subject
+ );
+
+/* TODO: conversation.sendMessage is to be rewritten so that it works when we don't show the WebView
+ /// Sends a text message in a given conversation.
+ void sendMessage(String text, {Map? custom}) {
+ final result = {};
+
+ if (custom != null) {
+ result['custom'] = custom;
+ }
+
+ session.execute('$variableName.sendMessage("$text", ${json.encode(result)});');
+ }
+ */
+
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+
+ if (!(other is Conversation)) {
+ return false;
+ }
+
+ if (_session != other._session) {
+ return false;
+ }
+
+ if (!setEquals(participants, other.participants)) {
+ return false;
+ }
+
+ if (id != other.id) {
+ return false;
+ }
+
+ if (!mapEquals(custom, other.custom)) {
+ return false;
+ }
+
+ if (!listEquals(welcomeMessages, other.welcomeMessages)) {
+ return false;
+ }
+
+ if (photoUrl != other.photoUrl) {
+ return false;
+ }
+
+ if (subject != other.subject) {
+ return false;
+ }
+
+ return true;
+ }
+
+ int get hashCode => hashValues(
+ _session,
+ hashList(participants),
+ id,
+ (custom != null ? hashList(custom!.keys) : custom),
+ (custom != null ? hashList(custom!.values) : custom),
+ (welcomeMessages != null ? hashList(welcomeMessages) : welcomeMessages),
+ photoUrl,
+ subject,
+ );
+}
+
+class ConversationData extends _BaseConversation {
+ ConversationData.fromJson(Map json)
+ : super(id: json['id'],
+ custom: (json['custom'] != null ? Map.from(json['custom']) : null),
+ welcomeMessages: (json['welcomeMessages'] != null ? List.from(json['welcomeMessages']) : null),
+ photoUrl: json['photoUrl'],
+ subject: json['subject']);
+}
+
diff --git a/lib/src/conversationlist.dart b/lib/src/conversationlist.dart
new file mode 100644
index 0000000..dec4714
--- /dev/null
+++ b/lib/src/conversationlist.dart
@@ -0,0 +1,303 @@
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/foundation.dart' show kDebugMode;
+
+import 'package:webview_flutter/webview_flutter.dart';
+
+import './session.dart';
+import './conversation.dart';
+import './user.dart';
+import './predicate.dart';
+
+typedef SelectConversationHandler = void Function(SelectConversationEvent event);
+
+class SelectConversationEvent {
+ final ConversationData conversation;
+ final UserData me;
+ final List others;
+
+ SelectConversationEvent.fromJson(Map json)
+ : conversation = ConversationData.fromJson(json['conversation']),
+ me = UserData.fromJson(json['me']),
+ others = json['others'].map((user) => UserData.fromJson(user)).toList();
+}
+
+class ConversationListOptions {
+ /// Controls if the feed header containing the toggle to enable desktop notifications is shown.
+ /// Defaults to true.
+ final bool? showFeedHeader;
+
+ /// Controls whether the user navigating between conversation should count
+ /// as steps in the browser history. Defaults to true, which means that if the user
+ /// clicks the browser's back button, they go back to the previous conversation
+ /// (if any).
+ ///
+ /// NOT NEEDED FOR FLUTTER?
+ //bool? useBrowserHistory;
+
+ /// Whether to show a "Back" button at the top of the chat screen on mobile devices.
+ ///
+ /// NOT NEEDED FOR FLUTTER?
+ //bool? showMobileBackButton;
+
+ /// Overrides the theme used for this chat UI.
+ final String? theme;
+
+ const ConversationListOptions({this.showFeedHeader, this.theme});
+
+ /// For internal use only. Implementation detail that may change anytime.
+ ///
+ /// This method is used instead of toJson for coherence with ChatBoxOptions.
+ /// The toJson method is intentionally omitted, to produce an error if
+ /// someone tries to convert this object to JSON instead of using the
+ /// getJsonString method.
+ String getJsonString(ConversationListState conversationList) {
+ final result = {};
+
+ if (showFeedHeader != null) {
+ result['showFeedHeader'] = showFeedHeader;
+ }
+
+ if (theme != null) {
+ result['theme'] = theme;
+ }
+
+ conversationList.setExtraOptions(result);
+
+ return json.encode(result);
+ }
+}
+
+class ConversationList extends StatefulWidget {
+ final Session session;
+
+ final bool? showFeedHeader;
+
+ final String? theme;
+
+ final ConversationPredicate feedFilter;
+
+ final SelectConversationHandler? onSelectConversation;
+
+ const ConversationList({
+ Key? key,
+ required this.session,
+ this.showFeedHeader,
+ this.theme,
+ this.feedFilter = const ConversationPredicate(),
+ this.onSelectConversation,
+ }) : super(key: key);
+
+ @override
+ State createState() => ConversationListState();
+}
+
+class ConversationListState extends State {
+ /// Used to control the underlying WebView
+ WebViewController? _webViewController;
+ bool _webViewCreated = false;
+
+ /// List of JavaScript statements that haven't been executed.
+ final _pending = [];
+
+ // A counter to ensure that IDs are unique
+ int _idCounter = 0;
+
+ /// A mapping of user ids to the variable name of the respective JavaScript
+ /// Talk.User object.
+ final _users = {};
+
+ /// Objects stored for comparing changes
+ ConversationPredicate _oldFeedFilter = const ConversationPredicate();
+
+ @override
+ Widget build(BuildContext context) {
+ if (kDebugMode) {
+ print('📗 conversationlist.build (_webViewCreated: $_webViewCreated)');
+ }
+
+ if (!_webViewCreated) {
+ _webViewCreated = true;
+
+ _createSession();
+ _createConversationList();
+ // feedFilter is set as an option for the inbox
+
+ execute('conversationList.mount(document.getElementById("talkjs-container"));');
+ } else {
+ // If it's not the first time that the widget is built,
+ // then check what needs to be rebuilt
+
+ // TODO: If something has changed in the Session we should do something
+ _checkFeedFilter();
+ }
+
+ return WebView(
+ initialUrl: 'about:blank',
+ javascriptMode: JavascriptMode.unrestricted,
+ debuggingEnabled: kDebugMode,
+ onWebViewCreated: _webViewCreatedCallback,
+ onPageFinished: _onPageFinished,
+ javascriptChannels: {
+ JavascriptChannel(name: 'JSCSelectConversation', onMessageReceived: _jscSelectConversation),
+ });
+ }
+
+ void _createSession() {
+ // Initialize Session object
+ final options = {};
+
+ options['appId'] = widget.session.appId;
+
+ if (widget.session.signature != null) {
+ options["signature"] = widget.session.signature;
+ }
+
+ execute('const options = ${json.encode(options)};');
+
+ final variableName = getUserVariableName(widget.session.me);
+ execute('options["me"] = $variableName;');
+
+ execute('const session = new Talk.Session(options);');
+ }
+
+ void _createConversationList() {
+ final options = ConversationListOptions(
+ showFeedHeader: widget.showFeedHeader,
+ theme: widget.theme,
+ );
+
+ _oldFeedFilter = ConversationPredicate.of(widget.feedFilter);
+
+ execute('const conversationList = session.createInbox(${options.getJsonString(this)});');
+
+ execute('''conversationList.on("selectConversation", (event) => {
+ event.preventDefault();
+ JSCSelectConversation.postMessage(JSON.stringify(event));
+ }); ''');
+ }
+
+ void _setFeedFilter() {
+ _oldFeedFilter = ConversationPredicate.of(widget.feedFilter);
+
+ execute('conversationList.setFeedFilter(${json.encode(_oldFeedFilter)});');
+ }
+
+ bool _checkFeedFilter() {
+ if (widget.feedFilter != _oldFeedFilter) {
+ _setFeedFilter();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ void _webViewCreatedCallback(WebViewController webViewController) async {
+ if (kDebugMode) {
+ print('📗 conversationlist._webViewCreatedCallback');
+ }
+
+ String htmlData = await rootBundle.loadString('packages/talkjs_flutter/assets/index.html');
+ Uri uri = Uri.dataFromString(htmlData, mimeType: 'text/html', encoding: Encoding.getByName('utf-8'));
+ webViewController.loadUrl(uri.toString());
+
+ _webViewController = webViewController;
+ }
+
+ void _onPageFinished(String url) {
+ if (kDebugMode) {
+ print('📗 conversationlist._onPageFinished');
+ }
+
+ if (url != 'about:blank') {
+ // Wait for TalkJS to be ready
+ // Not all WebViews support top level await, so it's better to use an
+ // async IIFE
+ final js = '(async function () { await Talk.ready; }());';
+
+ if (kDebugMode) {
+ print('📗 conversationlist._onPageFinished: $js');
+ }
+
+ _webViewController!.runJavascript(js);
+
+ // Execute any pending instructions
+ for (var statement in _pending) {
+ if (kDebugMode) {
+ print('📗 conversationlist._onPageFinished _pending: $statement');
+ }
+
+ _webViewController!.runJavascript(statement);
+ }
+ }
+ }
+
+ void _jscSelectConversation(JavascriptMessage message) {
+ if (kDebugMode) {
+ print('📗 conversationlist._jscSelectConversation: ${message.message}');
+ }
+
+ widget.onSelectConversation?.call(SelectConversationEvent.fromJson(json.decode(message.message)));
+ }
+
+ /// For internal use only. Implementation detail that may change anytime.
+ ///
+ /// Return a string with a unique ID
+ String getUniqueId() {
+ final id = _idCounter;
+
+ _idCounter += 1;
+
+ return '_$id';
+ }
+
+ /// For internal use only. Implementation detail that may change anytime.
+ ///
+ /// Returns the JavaScript variable name of the Talk.User object associated
+ /// with the given [User]
+ String getUserVariableName(User user) {
+ if (_users[user.id] == null) {
+ // Generate unique variable name
+ final variableName = 'user${getUniqueId()}';
+
+ execute('const $variableName = new Talk.User(${user.getJsonString()});');
+ _users[user.id] = variableName;
+ }
+
+ return _users[user.id]!;
+ }
+
+ /// For internal use only. Implementation detail that may change anytime.
+ ///
+ /// Sets the options for ConversationListOptions for the properties where there exists
+ /// both a declarative option and an imperative method
+ void setExtraOptions(Map result) {
+ result['feedFilter'] = widget.feedFilter;
+ }
+
+ /// For internal use only. Implementation detail that may change anytime.
+ ///
+ /// Evaluates the JavaScript statement given.
+ void execute(String statement) {
+ final controller = _webViewController;
+
+ if (kDebugMode) {
+ print('📘 conversationlist.execute: $statement');
+ }
+
+ if (controller != null) {
+ controller.runJavascript(statement);
+ } else {
+ this._pending.add(statement);
+ }
+ }
+
+ /// Destroys this UI element and removes all event listeners it has running.
+ void destroy() {
+ execute('conversationList.destroy();');
+ }
+}
+
diff --git a/lib/src/message.dart b/lib/src/message.dart
new file mode 100644
index 0000000..57df158
--- /dev/null
+++ b/lib/src/message.dart
@@ -0,0 +1,51 @@
+
+enum MessageType { UserMessage, SystemMessage }
+
+class Attachment {
+ final String url;
+ final int size;
+
+ Attachment.fromJson(Map json)
+ : url = json['url'],
+ size = json['size'];
+}
+
+class SentMessage {
+ /// The message ID of the message that was sent
+ final String? id;
+
+ /// The ID of the conversation that the message belongs to
+ final String conversationId;
+
+ /// Identifies the message as either a User message or System message
+ final MessageType type;
+
+ /// Contains an Array of User.id's that have read the message
+ final List readBy;
+
+ /// Contains the user ID for the person that sent the message
+ final String senderId; // redundant since the user is always me, but keeps it consistant
+
+ /// Contains the message's text
+ final String? text;
+
+ /// Only given if the message contains a file. An object with the URL and filesize (in bytes) of the given file.
+ final Attachment? attachment;
+
+ /// Only given if the message contains a location. An array of two numbers which represent the longitude and latitude of this location, respectively. Only given if this message is a shared location.
+ ///
+ /// Example:
+ /// [51.481083, -3.178306]
+ final List? location;
+
+ SentMessage.fromJson(Map json)
+ : id = json['id'],
+ conversationId = json['conversationId'],
+ type = (json['type'] == 'UserMessage' ? MessageType.UserMessage : MessageType.SystemMessage),
+ readBy = List.from(json['readBy']),
+ senderId = json['senderId'],
+ text = json['text'],
+ attachment = (json['attachment'] != null ? Attachment.fromJson(json['attachment']) : null),
+ location = (json['location'] != null ? List.from(json['location']) : null);
+}
+
diff --git a/lib/src/predicate.dart b/lib/src/predicate.dart
new file mode 100644
index 0000000..1b935d4
--- /dev/null
+++ b/lib/src/predicate.dart
@@ -0,0 +1,406 @@
+import 'dart:convert';
+import 'dart:ui';
+
+import 'package:flutter/foundation.dart';
+
+class FieldPredicate {
+ final String _operand;
+ String? _value;
+ List? _values;
+
+ FieldPredicate.equals(T value) : _operand = '==', _value = value.toString();
+ FieldPredicate.notEquals(T value) : _operand = '!=', _value = value.toString();
+ FieldPredicate.oneOf(List values) : _operand = 'oneOf', _values = values.map((value) => value.toString()).toList();
+ FieldPredicate.notOneOf(List values) : _operand = '!oneOf', _values = values.map((value) => value.toString()).toList();
+
+ FieldPredicate.of(FieldPredicate other)
+ : _operand = other._operand,
+ _value = other._value,
+ _values = (other._values != null ? List.of(other._values!) : null);
+
+ @override
+ String toString() {
+ return json.encode(this);
+ }
+
+ dynamic toJson() {
+ final result = [];
+
+ result.add(_operand);
+
+ if (_value != null) {
+ result.add(_value);
+ }
+
+ if (_values != null) {
+ result.add(_values);
+ }
+
+ return result;
+ }
+
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+
+ if (!(other is FieldPredicate)) {
+ return false;
+ }
+
+ if (_operand != other._operand) {
+ return false;
+ }
+
+ if (_value != other._value) {
+ return false;
+ }
+
+ if (!listEquals(_values, other._values)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ int get hashCode => hashValues(
+ _operand,
+ _value,
+ (_values != null ? hashList(_values) : _values),
+ );
+}
+
+class CustomFieldPredicate extends FieldPredicate {
+ bool? _exists;
+
+ CustomFieldPredicate.equals(String value) : super.equals(value);
+ CustomFieldPredicate.notEquals(String value) : super.notEquals(value);
+ CustomFieldPredicate.oneOf(List values) : super.oneOf(values);
+ CustomFieldPredicate.notOneOf(List values) : super.notOneOf(values);
+ CustomFieldPredicate.exists() : _exists = true, super.equals('');
+ CustomFieldPredicate.notExists() : _exists = false, super.notEquals('');
+
+ CustomFieldPredicate.of(CustomFieldPredicate other) : _exists = other._exists, super.of(other);
+
+ @override
+ dynamic toJson() {
+ if (_exists == null) {
+ return super.toJson();
+ } else if (_exists!) {
+ return 'exists';
+ } else {
+ return '!exists';
+ }
+ }
+
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+
+ if (!(other is CustomFieldPredicate)) {
+ return false;
+ }
+
+ if (_operand != other._operand) {
+ return false;
+ }
+
+ if (_value != other._value) {
+ return false;
+ }
+
+ if (!listEquals(_values, other._values)) {
+ return false;
+ }
+
+ if (_exists != other._exists) {
+ return false;
+ }
+
+ return true;
+ }
+
+ int get hashCode => hashValues(
+ _operand,
+ _value,
+ (_values != null ? hashList(_values) : _values),
+ _exists,
+ );
+}
+
+class ConversationAccessLevel {
+ final String _value;
+
+ const ConversationAccessLevel._(this._value);
+
+ static const ConversationAccessLevel none = ConversationAccessLevel._('None');
+ static const ConversationAccessLevel read = ConversationAccessLevel._('Read');
+ static const ConversationAccessLevel readWrite = ConversationAccessLevel._('ReadWrite');
+
+ @override
+ String toString() => _value;
+}
+
+class ConversationPredicate {
+ /// Only select conversations that the current user as specific access to.
+ final FieldPredicate? access;
+
+ /// Only select conversations that have particular custom fields set to particular values.
+ final Map? custom;
+
+ /// Set this field to only select conversations that have, or don't have any, unread messages.
+ final bool? hasUnreadMessages;
+
+ const ConversationPredicate({this.access, this.custom, this.hasUnreadMessages});
+
+ ConversationPredicate.of(ConversationPredicate other)
+ : access = (other.access != null ? FieldPredicate.of(other.access!) : null),
+ custom = (other.custom != null ? Map.of(other.custom!) : null),
+ hasUnreadMessages = other.hasUnreadMessages;
+
+ @override
+ String toString() {
+ return json.encode(this);
+ }
+
+ Map toJson() {
+ final result = {};
+
+ if (access != null) {
+ result['access'] = access;
+ }
+
+ if (custom != null) {
+ result['custom'] = custom;
+ }
+
+ if (hasUnreadMessages != null) {
+ result['hasUnreadMessages'] = hasUnreadMessages;
+ }
+
+ return result;
+ }
+
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+
+ if (!(other is ConversationPredicate)) {
+ return false;
+ }
+
+ if (access != other.access) {
+ return false;
+ }
+
+ if (!mapEquals(custom, other.custom)) {
+ return false;
+ }
+
+ if (hasUnreadMessages != other.hasUnreadMessages) {
+ return false;
+ }
+
+ return true;
+ }
+
+ int get hashCode => hashValues(
+ access,
+ (custom != null ? hashList(custom!.keys) : custom),
+ (custom != null ? hashList(custom!.values) : custom),
+ hasUnreadMessages,
+ );
+}
+
+class MessageOrigin {
+ final String _value;
+
+ const MessageOrigin._(this._value);
+
+ static const MessageOrigin web = MessageOrigin._('web');
+ static const MessageOrigin rest = MessageOrigin._('rest');
+ static const MessageOrigin email = MessageOrigin._('email');
+ static const MessageOrigin import = MessageOrigin._('import');
+
+ @override
+ String toString() => _value;
+}
+
+class MessageType {
+ final String _value;
+
+ const MessageType._(this._value);
+
+ static const MessageType userMessage = MessageType._('UserMessage');
+ static const MessageType systemMessage = MessageType._('SystemMessage');
+
+ @override
+ String toString() => _value;
+}
+
+class SenderPredicate {
+ final FieldPredicate? id;
+ final Map? custom;
+ final FieldPredicate? locale;
+ final FieldPredicate? role;
+
+ const SenderPredicate({this.id, this.custom, this.locale, this.role});
+
+ SenderPredicate.of(SenderPredicate other)
+ : id = (other.id != null ? FieldPredicate.of(other.id!) : null),
+ custom = (other.custom != null ? Map.of(other.custom!) : null),
+ locale = (other.locale != null ? FieldPredicate.of(other.locale!) : null),
+ role = (other.role != null ? FieldPredicate.of(other.role!) : null);
+
+ @override
+ String toString() {
+ return json.encode(this);
+ }
+
+ Map toJson() {
+ final result = {};
+
+ if (id != null) {
+ result['id'] = id;
+ }
+
+ if (custom != null) {
+ result['custom'] = custom;
+ }
+
+ if (locale != null) {
+ result['locale'] = locale;
+ }
+
+ if (role != null) {
+ result['role'] = role;
+ }
+
+ return result;
+ }
+
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+
+ if (!(other is SenderPredicate)) {
+ return false;
+ }
+
+ if (id != other.id) {
+ return false;
+ }
+
+ if (!mapEquals(custom, other.custom)) {
+ return false;
+ }
+
+ if (locale != other.locale) {
+ return false;
+ }
+
+ if (role != other.role) {
+ return false;
+ }
+
+ return true;
+ }
+
+ int get hashCode => hashValues(
+ id,
+ (custom != null ? hashList(custom!.keys) : custom),
+ (custom != null ? hashList(custom!.values) : custom),
+ locale,
+ role,
+ );
+}
+
+class MessagePredicate {
+ /// Only select messages that have particular custom fields set to particular values.
+ final Map? custom;
+
+ /// Only show messages that were sent by users (web), through the REST API (rest), via
+ /// reply-to-email (email) or via the import REST API (import).
+ final FieldPredicate? origin;
+
+ /// Only show messages that are sent by a sender that has all of the given properties
+ final SenderPredicate? sender;
+
+ /// Only show messages of a given type
+ final FieldPredicate? type;
+
+ const MessagePredicate({this.custom, this.origin, this.sender, this.type});
+
+ MessagePredicate.of(MessagePredicate other)
+ : custom = (other.custom != null ? Map.of(other.custom!) : null),
+ origin = (other.origin != null ? FieldPredicate.of(other.origin!) : null),
+ sender = (other.sender != null ? SenderPredicate.of(other.sender!) : null),
+ type = (other.type != null ? FieldPredicate.of(other.type!) : null);
+
+ @override
+ String toString() {
+ return json.encode(this);
+ }
+
+ Map toJson() {
+ final result = {};
+
+ if (custom != null) {
+ result['custom'] = custom;
+ }
+
+ if (origin != null) {
+ result['origin'] = origin;
+ }
+
+ if (sender != null) {
+ result['sender'] = sender;
+ }
+
+ if (type != null) {
+ result['type'] = type;
+ }
+
+ return result;
+ }
+
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+
+ if (!(other is MessagePredicate)) {
+ return false;
+ }
+
+ if (!mapEquals(custom, other.custom)) {
+ return false;
+ }
+
+ if (origin != other.origin) {
+ return false;
+ }
+
+ if (sender != other.sender) {
+ return false;
+ }
+
+ if (type != other.type) {
+ return false;
+ }
+
+ return true;
+ }
+
+ int get hashCode => hashValues(
+ (custom != null ? hashList(custom!.keys) : custom),
+ (custom != null ? hashList(custom!.values) : custom),
+ origin,
+ sender,
+ type,
+ );
+}
+
diff --git a/lib/src/session.dart b/lib/src/session.dart
new file mode 100644
index 0000000..dd299b8
--- /dev/null
+++ b/lib/src/session.dart
@@ -0,0 +1,84 @@
+import 'package:flutter/material.dart';
+
+import './user.dart';
+import './conversation.dart';
+
+/// A session represents a currently active user.
+class Session with ChangeNotifier {
+ /// Your TalkJS AppId that can be found your TalkJS [dashboard](https://talkjs.com/dashboard).
+ final String appId;
+
+ /// The TalkJS [User] associated with the current user in your application.
+ User? _me;
+
+ User get me {
+ if (_me == null) {
+ throw StateError('Set the me property before using the Session object');
+ } else {
+ return _me!;
+ }
+ }
+
+ set me(User user) {
+ if (_me != null) {
+ throw StateError('The me property has already been set for the Session object');
+ } else {
+ _me = user;
+ }
+ }
+
+ /// A digital signature of the current [User.id]
+ ///
+ /// This is the HMAC-SHA256 hash of the current user id, signed with your
+ /// TalkJS secret key.
+ /// DO NOT embed your secret key within your mobile application / frontend
+ /// code.
+ final String? signature;
+
+ Session({required this.appId, this.signature});
+
+ User getUser({
+ required String id,
+ required String name,
+ List? email,
+ List? phone,
+ String? availabilityText,
+ String? locale,
+ String? photoUrl,
+ String? role,
+ Map? custom,
+ String? welcomeMessage,
+ }) => User(
+ session: this,
+ id: id,
+ name: name,
+ email: email,
+ phone: phone,
+ availabilityText: availabilityText,
+ locale: locale,
+ photoUrl: photoUrl,
+ role: role,
+ custom: custom,
+ welcomeMessage: welcomeMessage,
+ );
+
+ User getUserById(String id) => User.fromId(id, this);
+
+ Conversation getConversation({
+ required String id,
+ Map? custom,
+ List? welcomeMessages,
+ String? photoUrl,
+ String? subject,
+ Set participants = const {},
+ }) => Conversation(
+ session: this,
+ id: id,
+ custom: custom,
+ welcomeMessages: welcomeMessages,
+ photoUrl: photoUrl,
+ subject: subject,
+ participants: participants,
+ );
+}
+
diff --git a/lib/src/user.dart b/lib/src/user.dart
new file mode 100644
index 0000000..76532e9
--- /dev/null
+++ b/lib/src/user.dart
@@ -0,0 +1,266 @@
+import 'dart:convert';
+import 'dart:ui';
+
+import 'package:flutter/foundation.dart';
+
+import './session.dart';
+
+/// A user of your app.
+///
+/// TalkJS uses the [id] to uniquely identify this user. All other fields of a
+/// [User] are allowed to vary over time and the TalkJS database will update its
+/// fields accordingly.
+class _BaseUser {
+ /// The default message a user sees when starting a chat with this person.
+ ///
+ /// This acts similarly to [welcomeMessage] with the difference being that
+ /// this appears as a system message.
+ final String? availabilityText;
+
+ /// Custom metadata for this user.
+ final Map? custom;
+
+ /// One or more email address belonging to this user.
+ ///
+ /// The email addresses will be used for [Email Notifications](https://talkjs.com/docs/Features/Notifications/Email_Notifications/index.html)
+ /// if they are enabled.
+ final List? email;
+
+ /// One or more phone numbers belonging to this user.
+ ///
+ /// The phone numbers will be used for [SMS Notifications](https://talkjs.com/docs/Features/Notifications/SMS_Notifications.html).
+ /// This feature requires standard plan and up.
+ final List? phone;
+
+ /// The unique user identifier.
+ final String id;
+
+ /// This user's name which will be displayed on the TalkJS UI
+ final String name;
+
+ /// The language on the UI.
+ ///
+ /// This field expects an [IETF language tag](https://www.w3.org/International/articles/language-tags/).
+ final String? locale;
+
+ /// An optional URL to a photo which will be displayed as this user's avatar
+ final String? photoUrl;
+
+ /// This user's role which allows you to change the behaviour of TalkJS for
+ /// different users.
+ final String? role;
+
+ /// The default message a user sees when starting a chat with this person.
+ final String? welcomeMessage;
+
+ const _BaseUser({
+ required this.id,
+ required this.name,
+ this.email,
+ this.phone,
+ this.availabilityText,
+ this.locale,
+ this.photoUrl,
+ this.role,
+ this.custom,
+ this.welcomeMessage,
+ });
+}
+
+class User extends _BaseUser {
+ // To support creating users with only an id
+ final bool _idOnly;
+
+ // To tie the user to a session
+ final Session _session;
+
+ const User({
+ required Session session,
+ required String id,
+ required String name,
+ List? email,
+ List? phone,
+ String? availabilityText,
+ String? locale,
+ String? photoUrl,
+ String? role,
+ Map? custom,
+ String? welcomeMessage,
+ })
+ : _session = session,
+ _idOnly = false,
+ super(
+ id: id,
+ name: name,
+ email: email,
+ phone: phone,
+ availabilityText: availabilityText,
+ locale: locale,
+ photoUrl: photoUrl,
+ role: role,
+ custom: custom,
+ welcomeMessage: welcomeMessage,
+ );
+
+ const User.fromId(String id, Session session) : _session = session, _idOnly = true, super(id: id, name: '');
+
+ User.of(User other)
+ : _session = other._session,
+ _idOnly = other._idOnly,
+ super(
+ id: other.id,
+ name: other.name,
+ email: (other.email != null ? List.of(other.email!) : null),
+ phone: (other.phone != null ? List.of(other.phone!) : null),
+ availabilityText: other.availabilityText,
+ locale: other.locale,
+ photoUrl: other.photoUrl,
+ role: other.role,
+ custom: (other.custom != null ? Map.of(other.custom!) : null),
+ welcomeMessage: other.welcomeMessage,
+ );
+
+ /// For internal use only. Implementation detail that may change anytime.
+ ///
+ /// This method is used instead of toJson, as we need to output valid JS
+ /// that is not valid JSON.
+ /// The toJson method is intentionally omitted, to produce an error if
+ /// someone tries to convert this object to JSON instead of using the
+ /// getJsonString method.
+ String getJsonString() {
+ if (this._idOnly) {
+ return '"$id"';
+ } else {
+ final result = {};
+
+ result['id'] = id;
+ result['name'] = name;
+
+ if (email != null) {
+ result['email'] = email;
+ }
+
+ if (phone != null) {
+ result['phone'] = phone;
+ }
+
+ if (availabilityText != null) {
+ result['availabilityText'] = availabilityText;
+ }
+
+ if (locale != null) {
+ result['locale'] = locale;
+ }
+
+ if (photoUrl != null) {
+ result['photoUrl'] = photoUrl;
+ }
+
+ if (role != null) {
+ result['role'] = role;
+ }
+
+ if (welcomeMessage != null) {
+ result['welcomeMessage'] = welcomeMessage;
+ }
+
+ if (custom != null) {
+ result['custom'] = custom;
+ }
+
+ return json.encode(result);
+ }
+ }
+
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+
+ if (!(other is User)) {
+ return false;
+ }
+
+ if (_session != other._session) {
+ return false;
+ }
+
+ if (_idOnly != other._idOnly) {
+ return false;
+ }
+
+ if (availabilityText != other.availabilityText) {
+ return false;
+ }
+
+ if (!mapEquals(custom, other.custom)) {
+ return false;
+ }
+
+ if (!listEquals(email, other.email)) {
+ return false;
+ }
+
+ if (!listEquals(phone, other.phone)) {
+ return false;
+ }
+
+ if (id != other.id) {
+ return false;
+ }
+
+ if (name != other.name) {
+ return false;
+ }
+
+ if (locale != other.locale) {
+ return false;
+ }
+
+ if (photoUrl != other.photoUrl) {
+ return false;
+ }
+
+ if (role != other.role) {
+ return false;
+ }
+
+ if (welcomeMessage != other.welcomeMessage) {
+ return false;
+ }
+
+ return true;
+ }
+
+ int get hashCode => hashValues(
+ _session,
+ _idOnly,
+ availabilityText,
+ (custom != null ? hashList(custom!.keys) : custom),
+ (custom != null ? hashList(custom!.values) : custom),
+ (email != null ? hashList(email) : email),
+ (phone != null ? hashList(phone) : phone),
+ id,
+ name,
+ locale,
+ photoUrl,
+ role,
+ welcomeMessage,
+ );
+}
+
+class UserData extends _BaseUser {
+ UserData.fromJson(Map json)
+ : super(availabilityText: json['availabilityText'],
+ custom: (json['custom'] != null ? Map.from(json['custom']) : null),
+ email: (json['email'] != null ? (json['email'] is String ? [json['email']] : List.from(json['email'])) : null),
+ phone: (json['phone'] != null ? (json['phone'] is String ? [json['phone']] : List.from(json['phone'])) : null),
+ id: json['id'],
+ name: json['name'],
+ locale: json['locale'],
+ photoUrl: json['photoUrl'],
+ role: json['role'],
+ welcomeMessage: json['welcomeMessage'],
+ );
+}
+
diff --git a/lib/talkjs.dart b/lib/talkjs.dart
new file mode 100644
index 0000000..14d0a6e
--- /dev/null
+++ b/lib/talkjs.dart
@@ -0,0 +1,32 @@
+library talkjs;
+
+import 'dart:convert' show json, utf8;
+import 'package:crypto/crypto.dart' show sha1;
+
+export 'src/chatoptions.dart';
+export 'src/conversation.dart';
+export 'src/session.dart';
+export 'src/chatbox.dart';
+export 'src/user.dart';
+export 'src/conversationlist.dart';
+export 'src/predicate.dart';
+
+/// The [Talk] object provides utility functions to help use TalkJS.
+class Talk {
+
+ /// Compute a Conversation ID based on participants' ids given.
+ ///
+ /// The order of the parameters does not matter.
+ /// Use this method if you want to simply create a conversation between two
+ /// users, not related to a particular product, order or transaction.
+ static String oneOnOneId(String me, String other) {
+ List ids = [me, other];
+ ids.sort();
+
+ final encoded = json.encode(ids);
+ final digest = sha1.convert(utf8.encode(encoded));
+
+ final hash = digest.toString().toLowerCase();
+ return hash.substring(0, 20);
+ }
+}
diff --git a/pubspec.lock b/pubspec.lock
new file mode 100644
index 0000000..0adf604
--- /dev/null
+++ b/pubspec.lock
@@ -0,0 +1,189 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ async:
+ dependency: transitive
+ description:
+ name: async
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.8.2"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ characters:
+ dependency: transitive
+ description:
+ name: characters
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ charcode:
+ dependency: transitive
+ description:
+ name: charcode
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.3.1"
+ clock:
+ dependency: transitive
+ description:
+ name: clock
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.15.0"
+ crypto:
+ dependency: "direct main"
+ description:
+ name: crypto
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
+ fake_async:
+ dependency: transitive
+ description:
+ name: fake_async
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ flutter:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_test:
+ dependency: "direct dev"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.12.11"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.7.0"
+ path:
+ dependency: transitive
+ description:
+ name: path
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.8.0"
+ plugin_platform_interface:
+ dependency: transitive
+ description:
+ name: plugin_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.2"
+ sky_engine:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.99"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.8.1"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.10.0"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.4.3"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.3.0"
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.1"
+ webview_flutter:
+ dependency: "direct main"
+ description:
+ name: webview_flutter
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.0"
+ webview_flutter_android:
+ dependency: transitive
+ description:
+ name: webview_flutter_android
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.8.2"
+ webview_flutter_platform_interface:
+ dependency: transitive
+ description:
+ name: webview_flutter_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.8.1"
+ webview_flutter_wkwebview:
+ dependency: transitive
+ description:
+ name: webview_flutter_wkwebview
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.7.1"
+sdks:
+ dart: ">=2.15.0 <3.0.0"
+ flutter: ">=2.8.1"
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..ea2d399
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,58 @@
+name: talkjs_flutter
+description: Official TalkJS SDK for Flutter
+version: 0.1.0
+homepage: https://talkjs.com
+
+environment:
+ sdk: ">=2.15.0 <3.0.0"
+ flutter: ">=2.8.1"
+
+dependencies:
+ flutter:
+ sdk: flutter
+
+ webview_flutter: ^3.0.0
+ crypto: ^3.0.1
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter.
+flutter:
+ assets:
+ - packages/talkjs_flutter/assets/index.html
+
+ # To add assets to your package, add an assets section, like this:
+ # assets:
+ # - images/a_dot_burr.jpeg
+ # - images/a_dot_ham.jpeg
+ #
+ # For details regarding assets in packages, see
+ # https://flutter.dev/assets-and-images/#from-packages
+ #
+ # An image asset can refer to one or more resolution-specific "variants", see
+ # https://flutter.dev/assets-and-images/#resolution-aware.
+
+ # To add custom fonts to your package, add a fonts section here,
+ # in this "flutter" section. Each entry in this list should have a
+ # "family" key with the font family name, and a "fonts" key with a
+ # list giving the asset and other descriptors for the font. For
+ # example:
+ # fonts:
+ # - family: Schyler
+ # fonts:
+ # - asset: fonts/Schyler-Regular.ttf
+ # - asset: fonts/Schyler-Italic.ttf
+ # style: italic
+ # - family: Trajan Pro
+ # fonts:
+ # - asset: fonts/TrajanPro.ttf
+ # - asset: fonts/TrajanPro_Bold.ttf
+ # weight: 700
+ #
+ # For details regarding fonts in packages, see
+ # https://flutter.dev/custom-fonts/#from-packages
diff --git a/test/talkjs_test.dart b/test/talkjs_test.dart
new file mode 100644
index 0000000..b7faab8
--- /dev/null
+++ b/test/talkjs_test.dart
@@ -0,0 +1,251 @@
+import 'dart:convert';
+
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:talkjs/talkjs.dart';
+
+void main() {
+ test('test oneOnOneId', () {
+ expect(Talk.oneOnOneId('1234', 'abcd'), '35ec37e6e0ca43ac8ccc');
+ expect(Talk.oneOnOneId('abcd', '1234'), '35ec37e6e0ca43ac8ccc');
+ });
+
+ test('test FieldPredicate ==', () {
+ expect(FieldPredicate.equals(ConversationAccessLevel.readWrite) == FieldPredicate.equals(ConversationAccessLevel.readWrite), true);
+ expect(FieldPredicate.notOneOf(['it', 'fr']) == FieldPredicate.notOneOf(['it', 'fr']), true);
+ });
+
+ test('test CustomFieldPredicate ==', () {
+ expect(CustomFieldPredicate.exists() == CustomFieldPredicate.exists(), true);
+ expect(CustomFieldPredicate.equals('it') == CustomFieldPredicate.equals('it'), true);
+ expect(CustomFieldPredicate.oneOf(['it', 'fr']) == CustomFieldPredicate.oneOf(['it', 'fr']), true);
+ });
+
+ test('test ConversationPredicate ==', () {
+ expect(
+ ConversationPredicate(
+ access: FieldPredicate.notEquals(ConversationAccessLevel.none),
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ hasUnreadMessages: false,
+ ) == ConversationPredicate(
+ access: FieldPredicate.notEquals(ConversationAccessLevel.none),
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ hasUnreadMessages: false,
+ )
+ , true);
+ });
+
+ test('test SenderPredicate ==', () {
+ expect(
+ SenderPredicate(
+ id: FieldPredicate.notEquals('INVALID_ID'),
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ locale: FieldPredicate.notOneOf(['it', 'fr']),
+ role: FieldPredicate.notEquals('admin'),
+ ) == SenderPredicate(
+ id: FieldPredicate.notEquals('INVALID_ID'),
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ locale: FieldPredicate.notOneOf(['it', 'fr']),
+ role: FieldPredicate.notEquals('admin'),
+ )
+ , true);
+ });
+
+ test('test MessagePredicate ==', () {
+ expect(
+ MessagePredicate(
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ origin: FieldPredicate.equals(MessageOrigin.web),
+ sender: SenderPredicate(
+ id: FieldPredicate.notEquals('INVALID_ID'),
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ locale: FieldPredicate.notOneOf(['it', 'fr']),
+ role: FieldPredicate.notEquals('admin'),
+ ),
+ type: FieldPredicate.notEquals(MessageType.systemMessage),
+ ) == MessagePredicate(
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ origin: FieldPredicate.equals(MessageOrigin.web),
+ sender: SenderPredicate(
+ id: FieldPredicate.notEquals('INVALID_ID'),
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ locale: FieldPredicate.notOneOf(['it', 'fr']),
+ role: FieldPredicate.notEquals('admin'),
+ ),
+ type: FieldPredicate.notEquals(MessageType.systemMessage),
+ )
+ , true);
+ });
+
+ test('test FieldPredicate.of', () {
+ expect(FieldPredicate.of(FieldPredicate.equals(ConversationAccessLevel.readWrite)) == FieldPredicate.equals(ConversationAccessLevel.readWrite), true);
+ expect(FieldPredicate.of(FieldPredicate.notOneOf(['it', 'fr'])) == FieldPredicate.notOneOf(['it', 'fr']), true);
+ });
+
+ test('test CustomFieldPredicate of', () {
+ expect(CustomFieldPredicate.of(CustomFieldPredicate.exists()) == CustomFieldPredicate.exists(), true);
+ expect(CustomFieldPredicate.of(CustomFieldPredicate.equals('it')) == CustomFieldPredicate.equals('it'), true);
+ expect(CustomFieldPredicate.of(CustomFieldPredicate.oneOf(['it', 'fr'])) == CustomFieldPredicate.oneOf(['it', 'fr']), true);
+ });
+
+ test('test ConversationPredicate of', () {
+ expect(
+ ConversationPredicate.of(ConversationPredicate(
+ access: FieldPredicate.notEquals(ConversationAccessLevel.none),
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ hasUnreadMessages: false,
+ )) == ConversationPredicate(
+ access: FieldPredicate.notEquals(ConversationAccessLevel.none),
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ hasUnreadMessages: false,
+ )
+ , true);
+ });
+
+ test('test SenderPredicate of', () {
+ expect(
+ SenderPredicate.of(SenderPredicate(
+ id: FieldPredicate.notEquals('INVALID_ID'),
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ locale: FieldPredicate.notOneOf(['it', 'fr']),
+ role: FieldPredicate.notEquals('admin'),
+ )) == SenderPredicate(
+ id: FieldPredicate.notEquals('INVALID_ID'),
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ locale: FieldPredicate.notOneOf(['it', 'fr']),
+ role: FieldPredicate.notEquals('admin'),
+ )
+ , true);
+ });
+
+ test('test MessagePredicate of', () {
+ expect(
+ MessagePredicate.of(MessagePredicate(
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ origin: FieldPredicate.equals(MessageOrigin.web),
+ sender: SenderPredicate(
+ id: FieldPredicate.notEquals('INVALID_ID'),
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ locale: FieldPredicate.notOneOf(['it', 'fr']),
+ role: FieldPredicate.notEquals('admin'),
+ ),
+ type: FieldPredicate.notEquals(MessageType.systemMessage),
+ )) == MessagePredicate(
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ origin: FieldPredicate.equals(MessageOrigin.web),
+ sender: SenderPredicate(
+ id: FieldPredicate.notEquals('INVALID_ID'),
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ locale: FieldPredicate.notOneOf(['it', 'fr']),
+ role: FieldPredicate.notEquals('admin'),
+ ),
+ type: FieldPredicate.notEquals(MessageType.systemMessage),
+ )
+ , true);
+ });
+
+ test('test ConversationPredicate string', () {
+ expect(
+ json.encode(ConversationPredicate(
+ access: FieldPredicate.notEquals(ConversationAccessLevel.none),
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ hasUnreadMessages: false,
+ )),
+ '{"access":["!=","None"],"custom":{"seller":"exists","category":["oneOf",["shoes","sandals"]],"visibility":["==","visible"]},"hasUnreadMessages":false}'
+ );
+ });
+
+ test('test MessagePredicate string', () {
+ expect(
+ json.encode(MessagePredicate(
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ origin: FieldPredicate.equals(MessageOrigin.web),
+ sender: SenderPredicate(
+ id: FieldPredicate.notEquals('INVALID_ID'),
+ custom: {
+ 'seller': CustomFieldPredicate.exists(),
+ 'category': CustomFieldPredicate.oneOf(['shoes', 'sandals']),
+ 'visibility': CustomFieldPredicate.equals('visible'),
+ },
+ locale: FieldPredicate.notOneOf(['it', 'fr']),
+ role: FieldPredicate.notEquals('admin'),
+ ),
+ type: FieldPredicate.notEquals(MessageType.systemMessage),
+ )),
+ '{"custom":{"seller":"exists","category":["oneOf",["shoes","sandals"]],"visibility":["==","visible"]},"origin":["==","web"],"sender":{"id":["!=","INVALID_ID"],"custom":{"seller":"exists","category":["oneOf",["shoes","sandals"]],"visibility":["==","visible"]},"locale":["!oneOf",["it","fr"]],"role":["!=","admin"]},"type":["!=","SystemMessage"]}'
+ );
+ });
+}
+