From 4f4b791deb99d9bb41c2d3d6bd2c136d7e0eea5f Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Tue, 18 May 2021 12:41:25 +0300 Subject: [PATCH 01/52] Project Setup Signed-off-by: Victor Omondi --- talkjs/.gitignore | 74 ++++++++++++++++++ talkjs/.metadata | 10 +++ talkjs/CHANGELOG.md | 3 + talkjs/LICENSE | 1 + talkjs/README.md | 14 ++++ talkjs/lib/talkjs.dart | 7 ++ talkjs/pubspec.lock | 147 +++++++++++++++++++++++++++++++++++ talkjs/pubspec.yaml | 54 +++++++++++++ talkjs/test/talkjs_test.dart | 12 +++ 9 files changed, 322 insertions(+) create mode 100644 talkjs/.gitignore create mode 100644 talkjs/.metadata create mode 100644 talkjs/CHANGELOG.md create mode 100644 talkjs/LICENSE create mode 100644 talkjs/README.md create mode 100644 talkjs/lib/talkjs.dart create mode 100644 talkjs/pubspec.lock create mode 100644 talkjs/pubspec.yaml create mode 100644 talkjs/test/talkjs_test.dart diff --git a/talkjs/.gitignore b/talkjs/.gitignore new file mode 100644 index 0000000..1985397 --- /dev/null +++ b/talkjs/.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/talkjs/.metadata b/talkjs/.metadata new file mode 100644 index 0000000..54f6c7b --- /dev/null +++ b/talkjs/.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/talkjs/CHANGELOG.md b/talkjs/CHANGELOG.md new file mode 100644 index 0000000..ac07159 --- /dev/null +++ b/talkjs/CHANGELOG.md @@ -0,0 +1,3 @@ +## [0.0.1] - TODO: Add release date. + +* TODO: Describe initial release. diff --git a/talkjs/LICENSE b/talkjs/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/talkjs/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/talkjs/README.md b/talkjs/README.md new file mode 100644 index 0000000..361207f --- /dev/null +++ b/talkjs/README.md @@ -0,0 +1,14 @@ +# talkjs + +A new Flutter package project. + +## Getting Started + +This project is a starting point for a Dart +[package](https://flutter.dev/developing-packages/), +a library module containing code that can be shared easily across +multiple Flutter or Dart projects. + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/talkjs/lib/talkjs.dart b/talkjs/lib/talkjs.dart new file mode 100644 index 0000000..921fd75 --- /dev/null +++ b/talkjs/lib/talkjs.dart @@ -0,0 +1,7 @@ +library talkjs; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/talkjs/pubspec.lock b/talkjs/pubspec.lock new file mode 100644 index 0000000..f1236ae --- /dev/null +++ b/talkjs/pubspec.lock @@ -0,0 +1,147 @@ +# 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.5.0" + 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.1.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + 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" + 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.10" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + 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.0" + 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.2.19" + 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.0" +sdks: + dart: ">=2.12.0-0.0 <3.0.0" + flutter: ">=1.17.0" diff --git a/talkjs/pubspec.yaml b/talkjs/pubspec.yaml new file mode 100644 index 0000000..d2d5dcf --- /dev/null +++ b/talkjs/pubspec.yaml @@ -0,0 +1,54 @@ +name: talkjs +description: A new Flutter package project. +version: 0.0.1 +author: +homepage: + +environment: + sdk: ">=2.7.0 <3.0.0" + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + +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: + + # 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/talkjs/test/talkjs_test.dart b/talkjs/test/talkjs_test.dart new file mode 100644 index 0000000..76dde46 --- /dev/null +++ b/talkjs/test/talkjs_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:talkjs/talkjs.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} From 49e26ed4daa6bd1069303e3668db5d348f46bc7d Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 11:05:42 +0300 Subject: [PATCH 02/52] Update SDK version constraints A Flutter version greater than 2.12.0 is required to be able to use Dart's null safety features. Signed-off-by: Victor Omondi --- talkjs/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/talkjs/pubspec.yaml b/talkjs/pubspec.yaml index d2d5dcf..15ef4c0 100644 --- a/talkjs/pubspec.yaml +++ b/talkjs/pubspec.yaml @@ -5,7 +5,7 @@ author: homepage: environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" flutter: ">=1.17.0" dependencies: From f15e8bb7fd79dfbda90722ed7c4dc45ebc05f9c9 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 11:09:42 +0300 Subject: [PATCH 03/52] Implement Talk class Signed-off-by: Victor Omondi --- talkjs/lib/talkjs.dart | 18 ++++++++++++++---- talkjs/pubspec.lock | 18 ++++++++++++++++-- talkjs/pubspec.yaml | 3 +++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/talkjs/lib/talkjs.dart b/talkjs/lib/talkjs.dart index 921fd75..7a52182 100644 --- a/talkjs/lib/talkjs.dart +++ b/talkjs/lib/talkjs.dart @@ -1,7 +1,17 @@ library talkjs; -/// A Calculator. -class Calculator { - /// Returns [value] plus 1. - int addOne(int value) => value + 1; +import 'dart:convert' show json, utf8; +import 'package:crypto/crypto.dart' show sha1; + +class Talk { + 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/talkjs/pubspec.lock b/talkjs/pubspec.lock index f1236ae..9f584f3 100644 --- a/talkjs/pubspec.lock +++ b/talkjs/pubspec.lock @@ -43,6 +43,13 @@ packages: 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: @@ -142,6 +149,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" sdks: - dart: ">=2.12.0-0.0 <3.0.0" - flutter: ">=1.17.0" + dart: ">=2.12.0 <3.0.0" + flutter: ">=1.22.0" diff --git a/talkjs/pubspec.yaml b/talkjs/pubspec.yaml index 15ef4c0..0a38c43 100644 --- a/talkjs/pubspec.yaml +++ b/talkjs/pubspec.yaml @@ -12,6 +12,9 @@ dependencies: flutter: sdk: flutter + webview_flutter: ^2.0.4 + crypto: ^3.0.1 + dev_dependencies: flutter_test: sdk: flutter From d32453652fadb37d354cdc9165ee6c6bcd9e05cb Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 11:15:36 +0300 Subject: [PATCH 04/52] Implement User class Signed-off-by: Victor Omondi --- talkjs/lib/src/user.dart | 34 ++++++++++++++++++++++++++++++++++ talkjs/lib/talkjs.dart | 2 ++ 2 files changed, 36 insertions(+) create mode 100644 talkjs/lib/src/user.dart diff --git a/talkjs/lib/src/user.dart b/talkjs/lib/src/user.dart new file mode 100644 index 0000000..07f9b6c --- /dev/null +++ b/talkjs/lib/src/user.dart @@ -0,0 +1,34 @@ +class User { + String? availabilityText; + Map? custom; + + List? email; + List? phone; + + String id; + String name; + + String? locale; + String? photoUrl; + + String? role; + String? welcomeMessage; + + User({required this.id, required this.name, this.email, this.phone, + this.availabilityText, this.locale, this.photoUrl, this.role, this.custom, + this.welcomeMessage + }); + + Map toJson() => { + 'id': id, + 'name': name, + 'email': email, + 'phone': phone, + 'availabilityText': availabilityText, + 'locale': locale, + 'photoUrl': photoUrl, + 'role': role, + 'welcomeMessage': welcomeMessage, + 'custom': custom + }; +} \ No newline at end of file diff --git a/talkjs/lib/talkjs.dart b/talkjs/lib/talkjs.dart index 7a52182..5723b36 100644 --- a/talkjs/lib/talkjs.dart +++ b/talkjs/lib/talkjs.dart @@ -3,6 +3,8 @@ library talkjs; import 'dart:convert' show json, utf8; import 'package:crypto/crypto.dart' show sha1; +export 'src/user.dart'; + class Talk { static String oneOnOneId(String me, String other) { List ids = [me, other]; From 1d6b7a7ef0aa185beb98750d1a2815250efbf211 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 11:20:57 +0300 Subject: [PATCH 05/52] Implement ChatWebView widget This widget acts like a simple wrapper around the WebView widget provided by webview_flutter. Signed-off-by: Victor Omondi --- talkjs/lib/src/webview.dart | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 talkjs/lib/src/webview.dart diff --git a/talkjs/lib/src/webview.dart b/talkjs/lib/src/webview.dart new file mode 100644 index 0000000..6a0a1fe --- /dev/null +++ b/talkjs/lib/src/webview.dart @@ -0,0 +1,41 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class ChatWebView extends StatefulWidget { + final ChatWebViewState state; + + ChatWebView(WebViewCreatedCallback webViewFn, PageFinishedCallback jsFn) + : state = ChatWebViewState(webViewFn, jsFn); + + @override + ChatWebViewState createState() => this.state; +} + +class ChatWebViewState extends State { + late WebView webView; + + ChatWebViewState(WebViewCreatedCallback webViewFn, + PageFinishedCallback jsFn) { + // Enable hybrid composition. + if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView(); + + this.webView = WebView( + initialUrl: '', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: webViewFn, + onPageFinished: jsFn, + ); + } + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return this.webView; + } +} \ No newline at end of file From 15bb9a0c7e7570e3d6854feff35d7d981bf8263f Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 11:24:19 +0300 Subject: [PATCH 06/52] Fix missing export Signed-off-by: Victor Omondi --- talkjs/lib/talkjs.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/talkjs/lib/talkjs.dart b/talkjs/lib/talkjs.dart index 5723b36..ed19b3e 100644 --- a/talkjs/lib/talkjs.dart +++ b/talkjs/lib/talkjs.dart @@ -4,6 +4,7 @@ import 'dart:convert' show json, utf8; import 'package:crypto/crypto.dart' show sha1; export 'src/user.dart'; +export 'src/webview.dart'; class Talk { static String oneOnOneId(String me, String other) { From 13b9efd5bccfdc72d4d5576375f90a2ce75500fb Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 11:28:16 +0300 Subject: [PATCH 07/52] Add html asset Signed-off-by: Victor Omondi --- talkjs/assets/index.html | 16 ++++++++++++++++ talkjs/pubspec.yaml | 2 ++ 2 files changed, 18 insertions(+) create mode 100644 talkjs/assets/index.html diff --git a/talkjs/assets/index.html b/talkjs/assets/index.html new file mode 100644 index 0000000..7e36806 --- /dev/null +++ b/talkjs/assets/index.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ + \ No newline at end of file diff --git a/talkjs/pubspec.yaml b/talkjs/pubspec.yaml index 0a38c43..0ca6140 100644 --- a/talkjs/pubspec.yaml +++ b/talkjs/pubspec.yaml @@ -24,6 +24,8 @@ dev_dependencies: # The following section is specific to Flutter. flutter: + assets: + - assets/index.html # To add assets to your package, add an assets section, like this: # assets: From 2e2003fedc1494e64f07f981d06a3dd256524b10 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 11:32:25 +0300 Subject: [PATCH 08/52] Implement Session class Signed-off-by: Victor Omondi --- talkjs/lib/src/session.dart | 85 +++++++++++++++++++++++++++++++++++++ talkjs/lib/talkjs.dart | 1 + 2 files changed, 86 insertions(+) create mode 100644 talkjs/lib/src/session.dart diff --git a/talkjs/lib/src/session.dart b/talkjs/lib/src/session.dart new file mode 100644 index 0000000..9f0196e --- /dev/null +++ b/talkjs/lib/src/session.dart @@ -0,0 +1,85 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +import './user.dart'; +import './webview.dart'; + +const chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'; + +class Session { + String appId; + User me; + late Widget chatUI; + + String? _signature; + + final List _pending = []; + WebViewController? _webViewController; + + Map _users = {}; + + Session({required this.appId, required this.me, this._signature}) { + this.chatUI = ChatWebView(_webViewCreatedCallback, _onPageFinished); + + // Initialize Session object + final options = {'appId': appId}; + execute('const options = ${json.encode(options)};'); + + final variableName = getUserName(this.me); + execute('options["me"] = $variableName;'); + + if (_signature != null) { + execute('options["signature"] = "$_signature";'); + } + + execute('const session = new Talk.Session(options);'); + } + + void _webViewCreatedCallback(WebViewController webViewController) async { + String htmlData = await rootBundle.loadString('assets/index.html'); + Uri uri = Uri.dataFromString(htmlData, mimeType: 'text/html', + encoding: Encoding.getByName('utf-8')); + webViewController.loadUrl(uri.toString()); + + this._webViewController = webViewController; + } + + void _onPageFinished(String url) { + if (url != 'about:blank') { + // Execute any pending instructions + for (var statement in this._pending) { + this._webViewController!.evaluateJavascript(statement); + } + } + } + + String getUserName(User user) { + if (_users[user.id] == null) { + // Generate random variable name + final rand = Random(); + final characters = List.generate( + 15, (index) => chars[rand.nextInt(chars.length)]); + final variableName = characters.join(); + + execute('const $variableName = new Talk.User(${json.encode(me)});'); + _users[user.id] = variableName; + } + + return _users[user.id]!; + } + + void execute(String statement) { + final controller = this._webViewController; + if (controller != null) { + controller.evaluateJavascript(statement); + } else { + this._pending.add(statement); + } + } + + void destroy() => execute('session.destroy();'); +} \ No newline at end of file diff --git a/talkjs/lib/talkjs.dart b/talkjs/lib/talkjs.dart index ed19b3e..03c1398 100644 --- a/talkjs/lib/talkjs.dart +++ b/talkjs/lib/talkjs.dart @@ -5,6 +5,7 @@ import 'package:crypto/crypto.dart' show sha1; export 'src/user.dart'; export 'src/webview.dart'; +export 'src/session.dart'; class Talk { static String oneOnOneId(String me, String other) { From 6973401f03271182aca48dde379d017863258381 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 11:34:46 +0300 Subject: [PATCH 09/52] Fix named parameter error in Session constructor Named parameters can't start with an underscore. This means the signature variable shall not be private. Signed-off-by: Victor Omondi --- talkjs/lib/src/session.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/talkjs/lib/src/session.dart b/talkjs/lib/src/session.dart index 9f0196e..22cd87c 100644 --- a/talkjs/lib/src/session.dart +++ b/talkjs/lib/src/session.dart @@ -15,14 +15,14 @@ class Session { User me; late Widget chatUI; - String? _signature; + String? signature; final List _pending = []; WebViewController? _webViewController; Map _users = {}; - Session({required this.appId, required this.me, this._signature}) { + Session({required this.appId, required this.me, this.signature}) { this.chatUI = ChatWebView(_webViewCreatedCallback, _onPageFinished); // Initialize Session object @@ -32,8 +32,8 @@ class Session { final variableName = getUserName(this.me); execute('options["me"] = $variableName;'); - if (_signature != null) { - execute('options["signature"] = "$_signature";'); + if (signature != null) { + execute('options["signature"] = "$signature";'); } execute('const session = new Talk.Session(options);'); From 2aed3651cfeab7cb90fd59d9945ca7650b318632 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 11:37:53 +0300 Subject: [PATCH 10/52] Implement ConversationBuilder class Signed-off-by: Victor Omondi --- talkjs/lib/src/conversation.dart | 103 +++++++++++++++++++++++++++++++ talkjs/lib/src/session.dart | 7 +++ talkjs/lib/talkjs.dart | 1 + 3 files changed, 111 insertions(+) create mode 100644 talkjs/lib/src/conversation.dart diff --git a/talkjs/lib/src/conversation.dart b/talkjs/lib/src/conversation.dart new file mode 100644 index 0000000..d3eb61d --- /dev/null +++ b/talkjs/lib/src/conversation.dart @@ -0,0 +1,103 @@ +import 'dart:convert'; + +import './session.dart'; +import './user.dart'; + +class ConversationBuilder { + Map? custom; + List? welcomeMessages; + + String? photoUrl; + String? subject; + + Session session; + String variableName; + + ConversationBuilder({required this.session, required this.variableName, + this.custom, this.welcomeMessages, this.photoUrl, this.subject, + }); + + void sendMessage(String text, MessageOptions options) { + session.execute( + '$variableName.sendMessage("$text", ${json.encode(options)});'); + } + + void setAttributes(ConversationAttributes attributes) { + session.execute('$variableName.setAttributes(${json.encode(attributes)});'); + } + + void setParticipant(User user, {ParticipantSettings? participantSettings}) { + final userName = session.getUserName(user); + final settings = participantSettings ?? {}; + session.execute( + '$variableName.setParticipant($userName, ${json.encode(settings)});' + 'console.log("$userName: ${user.id}");'); + } +} + +class MessageOptions { + Map? custom; + + MessageOptions({this.custom}); + + Map toJson() => { + 'custom': custom ?? {} + }; +} + +class ConversationAttributes { + Map? custom; + List? welcomeMessages; + + String? photoUrl; + String? subject; + + ConversationAttributes({this.custom, this.welcomeMessages, this.photoUrl, + this.subject + }); + + Map toJson() => { + 'custom': custom, + 'welcomeMessages': welcomeMessages, + 'photoUrl': photoUrl, + 'subject': subject + }; +} + +enum Access { read, readWrite } + +extension StringConversion on Access { + String getValue() { + late String result; + switch (this) { + case Access.read: + result = 'Read'; + break; + case Access.readWrite: + result = 'ReadWrite'; + break; + } + return result; + } +} + +class ParticipantSettings { + final Access? access; + final bool? notify; + + const ParticipantSettings({this.access, this.notify}); + + Map toJson() { + final Map result = {}; + + if (access != null) { + result['access'] = access!.getValue(); + } + + if (notify != null) { + result['notify'] = notify; + } + + return result; + } +} \ No newline at end of file diff --git a/talkjs/lib/src/session.dart b/talkjs/lib/src/session.dart index 22cd87c..6fc18d3 100644 --- a/talkjs/lib/src/session.dart +++ b/talkjs/lib/src/session.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import './conversation.dart'; import './user.dart'; import './webview.dart'; @@ -82,4 +83,10 @@ class Session { } void destroy() => execute('session.destroy();'); + + ConversationBuilder getOrCreateConversation(String conversationId) { + execute( + 'const conversation = session.getOrCreateConversation("$conversationId")'); + return ConversationBuilder(session: this, variableName: 'conversation'); + } } \ No newline at end of file diff --git a/talkjs/lib/talkjs.dart b/talkjs/lib/talkjs.dart index 03c1398..37bbc66 100644 --- a/talkjs/lib/talkjs.dart +++ b/talkjs/lib/talkjs.dart @@ -3,6 +3,7 @@ library talkjs; import 'dart:convert' show json, utf8; import 'package:crypto/crypto.dart' show sha1; +export 'src/conversation.dart'; export 'src/user.dart'; export 'src/webview.dart'; export 'src/session.dart'; From 54f93da48c63fe7446d6372e604da41de8190e08 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 11:43:51 +0300 Subject: [PATCH 11/52] Implement ChatBox Signed-off-by: Victor Omondi --- talkjs/lib/src/chatbox.dart | 107 ++++++++++++++++++++++++++++++++++++ talkjs/lib/src/session.dart | 11 ++++ talkjs/lib/talkjs.dart | 1 + 3 files changed, 119 insertions(+) create mode 100644 talkjs/lib/src/chatbox.dart diff --git a/talkjs/lib/src/chatbox.dart b/talkjs/lib/src/chatbox.dart new file mode 100644 index 0000000..44d4f32 --- /dev/null +++ b/talkjs/lib/src/chatbox.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; + +import './session.dart'; + +class ChatBox { + late Session session; + late String variableName; + + ChatBox({required this.session, required this.variableName}); + + void destroy() { + session.execute('$variableName.destroy();'); + } + + Widget mount() { + session.execute( + '$variableName.mount(document.getElementById("talkjs-container"));'); + return session.chatUI; + } +} + +enum ChatMode { subject, participants } + +extension ChatModeString on ChatMode { + String getValue() { + late String result; + switch (this) { + case ChatMode.participants: + result = 'participants'; + break; + case ChatMode.subject: + result = 'subject'; + break; + } + + return result; + } +} + +enum TextDirection { rtl, ltr } + +extension TextDirectionString on TextDirection { + String getValue() { + late String result; + switch (this) { + case TextDirection.rtl: + result = 'rtl'; + break; + case TextDirection.ltr: + result = 'ltr'; + break; + } + + return result; + } +} + +class MessageFieldOptions { + bool autofocus; // Convert to "smart" + bool enterSendsMessage; + + String? placeholder; + bool spellcheck; + + MessageFieldOptions({this.autofocus = true, this.enterSendsMessage = true, + this.placeholder, this.spellcheck = false + }); + + Map toJson() { + final result = { + 'enterSendsMessage': enterSendsMessage, + 'placeholder': placeholder, + 'spellcheck': spellcheck + }; + + if (autofocus == true) { + result['autofocus'] = 'smart'; + } else { + result['autofocus'] = autofocus; + } + + return result; + } +} + +class ChatBoxOptions { + ChatMode chatSubtitleMode; + ChatMode chatTitleMode; + + TextDirection dir; + bool showChatHeader; + + MessageFieldOptions? messageField; + + ChatBoxOptions({this.chatSubtitleMode = ChatMode.subject, + this.chatTitleMode = ChatMode.participants, this.dir = TextDirection.rtl, + this.showChatHeader = true, this.messageField + }); + + Map toJson() => { + 'chatSubtitleMode': chatSubtitleMode.getValue(), + 'chatTitleMode': chatTitleMode.getValue(), + 'dir': dir.getValue(), + 'showChatHeader': showChatHeader, + 'messageField': messageField ?? {} + }; +} \ No newline at end of file diff --git a/talkjs/lib/src/session.dart b/talkjs/lib/src/session.dart index 6fc18d3..9072305 100644 --- a/talkjs/lib/src/session.dart +++ b/talkjs/lib/src/session.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import './chatbox.dart'; import './conversation.dart'; import './user.dart'; import './webview.dart'; @@ -89,4 +90,14 @@ class Session { 'const conversation = session.getOrCreateConversation("$conversationId")'); return ConversationBuilder(session: this, variableName: 'conversation'); } + + ChatBox createChatbox( + ConversationBuilder selectedConversation, + {ChatBoxOptions? chatBoxOptions}) { + final options = chatBoxOptions ?? {}; + execute('const chatBox = session.createChatbox(' + '${selectedConversation.variableName}, ${json.encode(options)});'); + + return ChatBox(session: this, variableName: 'chatBox'); + } } \ No newline at end of file diff --git a/talkjs/lib/talkjs.dart b/talkjs/lib/talkjs.dart index 37bbc66..0d773af 100644 --- a/talkjs/lib/talkjs.dart +++ b/talkjs/lib/talkjs.dart @@ -3,6 +3,7 @@ library talkjs; import 'dart:convert' show json, utf8; import 'package:crypto/crypto.dart' show sha1; +export 'src/chatbox.dart'; export 'src/conversation.dart'; export 'src/user.dart'; export 'src/webview.dart'; From e21bac6adf3dd58fad0db22032db0b18e322c6ce Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 12:24:39 +0300 Subject: [PATCH 12/52] Refactor ChatBoxOptions Since ChatBoxOptions and InboxOptions are very similar, it makes sense to have them share a superclass. The ChatOptions class will serve this purpose. Signed-off-by: Victor Omondi --- talkjs/lib/src/chatbox.dart | 87 ------------------------------ talkjs/lib/src/chatoptions.dart | 94 +++++++++++++++++++++++++++++++++ talkjs/lib/src/session.dart | 1 + 3 files changed, 95 insertions(+), 87 deletions(-) create mode 100644 talkjs/lib/src/chatoptions.dart diff --git a/talkjs/lib/src/chatbox.dart b/talkjs/lib/src/chatbox.dart index 44d4f32..5a6e103 100644 --- a/talkjs/lib/src/chatbox.dart +++ b/talkjs/lib/src/chatbox.dart @@ -17,91 +17,4 @@ class ChatBox { '$variableName.mount(document.getElementById("talkjs-container"));'); return session.chatUI; } -} - -enum ChatMode { subject, participants } - -extension ChatModeString on ChatMode { - String getValue() { - late String result; - switch (this) { - case ChatMode.participants: - result = 'participants'; - break; - case ChatMode.subject: - result = 'subject'; - break; - } - - return result; - } -} - -enum TextDirection { rtl, ltr } - -extension TextDirectionString on TextDirection { - String getValue() { - late String result; - switch (this) { - case TextDirection.rtl: - result = 'rtl'; - break; - case TextDirection.ltr: - result = 'ltr'; - break; - } - - return result; - } -} - -class MessageFieldOptions { - bool autofocus; // Convert to "smart" - bool enterSendsMessage; - - String? placeholder; - bool spellcheck; - - MessageFieldOptions({this.autofocus = true, this.enterSendsMessage = true, - this.placeholder, this.spellcheck = false - }); - - Map toJson() { - final result = { - 'enterSendsMessage': enterSendsMessage, - 'placeholder': placeholder, - 'spellcheck': spellcheck - }; - - if (autofocus == true) { - result['autofocus'] = 'smart'; - } else { - result['autofocus'] = autofocus; - } - - return result; - } -} - -class ChatBoxOptions { - ChatMode chatSubtitleMode; - ChatMode chatTitleMode; - - TextDirection dir; - bool showChatHeader; - - MessageFieldOptions? messageField; - - ChatBoxOptions({this.chatSubtitleMode = ChatMode.subject, - this.chatTitleMode = ChatMode.participants, this.dir = TextDirection.rtl, - this.showChatHeader = true, this.messageField - }); - - Map toJson() => { - 'chatSubtitleMode': chatSubtitleMode.getValue(), - 'chatTitleMode': chatTitleMode.getValue(), - 'dir': dir.getValue(), - 'showChatHeader': showChatHeader, - 'messageField': messageField ?? {} - }; } \ No newline at end of file diff --git a/talkjs/lib/src/chatoptions.dart b/talkjs/lib/src/chatoptions.dart new file mode 100644 index 0000000..c1ccd67 --- /dev/null +++ b/talkjs/lib/src/chatoptions.dart @@ -0,0 +1,94 @@ +enum ChatMode { subject, participants } + +extension ChatModeString on ChatMode { + String getValue() { + late String result; + switch (this) { + case ChatMode.participants: + result = 'participants'; + break; + case ChatMode.subject: + result = 'subject'; + break; + } + + return result; + } +} + +enum TextDirection { rtl, ltr } + +extension TextDirectionString on TextDirection { + String getValue() { + late String result; + switch (this) { + case TextDirection.rtl: + result = 'rtl'; + break; + case TextDirection.ltr: + result = 'ltr'; + break; + } + + return result; + } +} + +class MessageFieldOptions { + bool autofocus; // Convert to "smart" + bool enterSendsMessage; + + String? placeholder; + bool spellcheck; + + MessageFieldOptions({this.autofocus = true, this.enterSendsMessage = true, + this.placeholder, this.spellcheck = false + }); + + Map toJson() { + final result = { + 'enterSendsMessage': enterSendsMessage, + 'placeholder': placeholder, + 'spellcheck': spellcheck + }; + + if (autofocus == true) { + result['autofocus'] = 'smart'; + } else { + result['autofocus'] = autofocus; + } + + return result; + } +} + +class ChatOptions { + ChatMode chatSubtitleMode; + ChatMode chatTitleMode; + + TextDirection dir; + bool showChatHeader; + + MessageFieldOptions? messageField; + + ChatOptions({this.chatSubtitleMode = ChatMode.subject, + this.chatTitleMode = ChatMode.participants, this.dir = TextDirection.rtl, + this.showChatHeader = true, this.messageField + }); + + Map toJson() => { + 'chatSubtitleMode': chatSubtitleMode.getValue(), + 'chatTitleMode': chatTitleMode.getValue(), + 'dir': dir.getValue(), + 'showChatHeader': showChatHeader, + 'messageField': messageField ?? {} + }; +} + +class ChatBoxOptions extends ChatOptions{ + ChatBoxOptions({chatSubtitleMode, chatTitleMode, dir, showChatHeader, + messageField}) + : super(chatSubtitleMode: chatSubtitleMode, chatTitleMode: chatTitleMode, + dir: dir, showChatHeader: showChatHeader, + messageField: messageField); +} \ No newline at end of file diff --git a/talkjs/lib/src/session.dart b/talkjs/lib/src/session.dart index 9072305..5ede337 100644 --- a/talkjs/lib/src/session.dart +++ b/talkjs/lib/src/session.dart @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; import 'package:webview_flutter/webview_flutter.dart'; import './chatbox.dart'; +import './chatoptions.dart'; import './conversation.dart'; import './user.dart'; import './webview.dart'; From fd0e6a01ff030faa25e03440f29cea3a376bd574 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 12:40:03 +0300 Subject: [PATCH 13/52] Refactor ChatBox The ChatBox and Inbox classes are very similar and share a lot of methods. With this in mind, having a class that they both extend helps prevent having unnecessary redundant code. Signed-off-by: Victor Omondi --- talkjs/lib/src/session.dart | 2 +- talkjs/lib/src/{chatbox.dart => ui.dart} | 13 +++++++++---- talkjs/lib/talkjs.dart | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) rename talkjs/lib/src/{chatbox.dart => ui.dart} (55%) diff --git a/talkjs/lib/src/session.dart b/talkjs/lib/src/session.dart index 5ede337..b309b48 100644 --- a/talkjs/lib/src/session.dart +++ b/talkjs/lib/src/session.dart @@ -5,9 +5,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:webview_flutter/webview_flutter.dart'; -import './chatbox.dart'; import './chatoptions.dart'; import './conversation.dart'; +import './ui.dart'; import './user.dart'; import './webview.dart'; diff --git a/talkjs/lib/src/chatbox.dart b/talkjs/lib/src/ui.dart similarity index 55% rename from talkjs/lib/src/chatbox.dart rename to talkjs/lib/src/ui.dart index 5a6e103..d097c91 100644 --- a/talkjs/lib/src/chatbox.dart +++ b/talkjs/lib/src/ui.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import './session.dart'; -class ChatBox { - late Session session; - late String variableName; +class UI { + Session session; + String variableName; - ChatBox({required this.session, required this.variableName}); + UI({required this.session, required this.variableName}); void destroy() { session.execute('$variableName.destroy();'); @@ -17,4 +17,9 @@ class ChatBox { '$variableName.mount(document.getElementById("talkjs-container"));'); return session.chatUI; } +} + +class ChatBox extends UI { + ChatBox({session, variableName}) + : super(session: session, variableName: variableName); } \ No newline at end of file diff --git a/talkjs/lib/talkjs.dart b/talkjs/lib/talkjs.dart index 0d773af..a5bc27d 100644 --- a/talkjs/lib/talkjs.dart +++ b/talkjs/lib/talkjs.dart @@ -3,7 +3,7 @@ library talkjs; import 'dart:convert' show json, utf8; import 'package:crypto/crypto.dart' show sha1; -export 'src/chatbox.dart'; +export 'src/ui.dart'; export 'src/conversation.dart'; export 'src/user.dart'; export 'src/webview.dart'; From 09a6a20df1864cea6689b17f1cde170c8972a8ea Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 12:43:34 +0300 Subject: [PATCH 14/52] Set UI and ChatOptions as abstract These two classes don't need to be instantiated directly hence they deserve to be abstract classes. Signed-off-by: Victor Omondi --- talkjs/lib/src/chatoptions.dart | 2 +- talkjs/lib/src/ui.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/talkjs/lib/src/chatoptions.dart b/talkjs/lib/src/chatoptions.dart index c1ccd67..3de073c 100644 --- a/talkjs/lib/src/chatoptions.dart +++ b/talkjs/lib/src/chatoptions.dart @@ -62,7 +62,7 @@ class MessageFieldOptions { } } -class ChatOptions { +abstract class ChatOptions { ChatMode chatSubtitleMode; ChatMode chatTitleMode; diff --git a/talkjs/lib/src/ui.dart b/talkjs/lib/src/ui.dart index d097c91..0ef0dec 100644 --- a/talkjs/lib/src/ui.dart +++ b/talkjs/lib/src/ui.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import './session.dart'; -class UI { +abstract class UI { Session session; String variableName; From a0e45f3534475fffb17416dbe1049d40ca5ab116 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 12:46:48 +0300 Subject: [PATCH 15/52] Fix missing chatoptions export Also rearranged the order of the exports to be alphabetical Signed-off-by: Victor Omondi --- talkjs/lib/talkjs.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/talkjs/lib/talkjs.dart b/talkjs/lib/talkjs.dart index a5bc27d..0496291 100644 --- a/talkjs/lib/talkjs.dart +++ b/talkjs/lib/talkjs.dart @@ -3,11 +3,12 @@ library talkjs; import 'dart:convert' show json, utf8; import 'package:crypto/crypto.dart' show sha1; -export 'src/ui.dart'; +export 'src/chatoptions.dart'; export 'src/conversation.dart'; +export 'src/session.dart'; +export 'src/ui.dart'; export 'src/user.dart'; export 'src/webview.dart'; -export 'src/session.dart'; class Talk { static String oneOnOneId(String me, String other) { From 21aeeb8741eae84d915f01c7e5f2107ecd5bacf3 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 12:47:26 +0300 Subject: [PATCH 16/52] Implement Inbox Signed-off-by: Victor Omondi --- talkjs/lib/src/ui.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/talkjs/lib/src/ui.dart b/talkjs/lib/src/ui.dart index 0ef0dec..1437f3c 100644 --- a/talkjs/lib/src/ui.dart +++ b/talkjs/lib/src/ui.dart @@ -22,4 +22,9 @@ abstract class UI { class ChatBox extends UI { ChatBox({session, variableName}) : super(session: session, variableName: variableName); +} + +class Inbox extends UI { + Inbox({session, variableName}) + : super(session: session, variableName: variableName); } \ No newline at end of file From 54f3942132670926db8b653ff47c839989e3078c Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 12:54:08 +0300 Subject: [PATCH 17/52] Implement session.createInbox method Signed-off-by: Victor Omondi --- talkjs/lib/src/chatoptions.dart | 8 ++++++++ talkjs/lib/src/session.dart | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/talkjs/lib/src/chatoptions.dart b/talkjs/lib/src/chatoptions.dart index 3de073c..f12da14 100644 --- a/talkjs/lib/src/chatoptions.dart +++ b/talkjs/lib/src/chatoptions.dart @@ -91,4 +91,12 @@ class ChatBoxOptions extends ChatOptions{ : super(chatSubtitleMode: chatSubtitleMode, chatTitleMode: chatTitleMode, dir: dir, showChatHeader: showChatHeader, messageField: messageField); +} + +class InboxOptions extends ChatOptions { + InboxOptions({chatSubtitleMode, chatTitleMode, dir, showChatHeader, + messageField}) + : super(chatSubtitleMode: chatSubtitleMode, chatTitleMode: chatTitleMode, + dir: dir, showChatHeader: showChatHeader, + messageField: messageField); } \ No newline at end of file diff --git a/talkjs/lib/src/session.dart b/talkjs/lib/src/session.dart index b309b48..6a35fc5 100644 --- a/talkjs/lib/src/session.dart +++ b/talkjs/lib/src/session.dart @@ -101,4 +101,11 @@ class Session { return ChatBox(session: this, variableName: 'chatBox'); } + + Inbox createInbox({InboxOptions? inboxOptions}) { + final options = inboxOptions ?? {}; + execute('const inbox = session.createInbox(${json.encode(options)});'); + + return Inbox(session: this, variableName: 'inbox'); + } } \ No newline at end of file From 167dc9a84013467b02c2782ec3e3f6afc7084772 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 13:08:06 +0300 Subject: [PATCH 18/52] Implement session.createPopup method Signed-off-by: Victor Omondi --- talkjs/lib/src/chatoptions.dart | 10 ++++++++++ talkjs/lib/src/session.dart | 11 +++++++++++ talkjs/lib/src/ui.dart | 5 +++++ 3 files changed, 26 insertions(+) diff --git a/talkjs/lib/src/chatoptions.dart b/talkjs/lib/src/chatoptions.dart index f12da14..007637f 100644 --- a/talkjs/lib/src/chatoptions.dart +++ b/talkjs/lib/src/chatoptions.dart @@ -99,4 +99,14 @@ class InboxOptions extends ChatOptions { : super(chatSubtitleMode: chatSubtitleMode, chatTitleMode: chatTitleMode, dir: dir, showChatHeader: showChatHeader, messageField: messageField); +} + +class PopupOptions extends ChatOptions { + bool keepOpen; + + PopupOptions({this.keepOpen = false, chatSubtitleMode, chatTitleMode, dir, + showChatHeader, messageField}) + : super(chatSubtitleMode: chatSubtitleMode, chatTitleMode: chatTitleMode, + dir: dir, showChatHeader: showChatHeader, + messageField: messageField); } \ No newline at end of file diff --git a/talkjs/lib/src/session.dart b/talkjs/lib/src/session.dart index 6a35fc5..00d823f 100644 --- a/talkjs/lib/src/session.dart +++ b/talkjs/lib/src/session.dart @@ -108,4 +108,15 @@ class Session { return Inbox(session: this, variableName: 'inbox'); } + + Popup createPopup( + ConversationBuilder conversation, {PopupOptions? popupOptions}) { + final options = popupOptions ?? {}; + final variableName = 'popup'; + + execute('const $variableName = session.createPopup(' + '${conversation.variableName}, ${json.encode(options)});'); + + return Popup(session: this, variableName: variableName); + } } \ No newline at end of file diff --git a/talkjs/lib/src/ui.dart b/talkjs/lib/src/ui.dart index 1437f3c..9a323ef 100644 --- a/talkjs/lib/src/ui.dart +++ b/talkjs/lib/src/ui.dart @@ -27,4 +27,9 @@ class ChatBox extends UI { class Inbox extends UI { Inbox({session, variableName}) : super(session: session, variableName: variableName); +} + +class Popup extends UI { + Popup({session, variableName}) + : super(session: session, variableName: variableName); } \ No newline at end of file From d4dfbc4e4df9f9caf916ddb6b3a50e25db866b6f Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 13:22:06 +0300 Subject: [PATCH 19/52] Implement test for oneOnOneId method Signed-off-by: Victor Omondi --- talkjs/test/talkjs_test.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/talkjs/test/talkjs_test.dart b/talkjs/test/talkjs_test.dart index 76dde46..e407744 100644 --- a/talkjs/test/talkjs_test.dart +++ b/talkjs/test/talkjs_test.dart @@ -3,10 +3,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:talkjs/talkjs.dart'; void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); + test('test oneOnOneId', () { + expect(Talk.oneOnOneId('1234', 'abcd'), '35ec37e6e0ca43ac8ccc'); + expect(Talk.oneOnOneId('abcd', '1234'), '35ec37e6e0ca43ac8ccc'); }); } From 78809bb4c484e16eca99fb43f2a8dba71ffc1ff7 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 16:24:57 +0300 Subject: [PATCH 20/52] Refactor index.html The css changes implemented fix the issue where the Chat's height wasn't covering the whole height of the WebView widget. It also removes the small margins to the left and right of the chatbox. Signed-off-by: Victor Omondi --- talkjs/{ => lib}/assets/index.html | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) rename talkjs/{ => lib}/assets/index.html (66%) diff --git a/talkjs/assets/index.html b/talkjs/lib/assets/index.html similarity index 66% rename from talkjs/assets/index.html rename to talkjs/lib/assets/index.html index 7e36806..78dfe63 100644 --- a/talkjs/assets/index.html +++ b/talkjs/lib/assets/index.html @@ -3,6 +3,22 @@ + -
+
\ No newline at end of file From fc82f9967a1c4f7fb9145d1a29cfebcbd554ce17 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 16:36:38 +0300 Subject: [PATCH 21/52] Fix asset not loading When the package is used in an app, the index.html asset was not getting loaded. This was due to an incorrect path in the pubspec.yaml file Signed-off-by: Victor Omondi --- talkjs/lib/src/session.dart | 3 ++- talkjs/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/talkjs/lib/src/session.dart b/talkjs/lib/src/session.dart index 00d823f..71ae518 100644 --- a/talkjs/lib/src/session.dart +++ b/talkjs/lib/src/session.dart @@ -43,7 +43,8 @@ class Session { } void _webViewCreatedCallback(WebViewController webViewController) async { - String htmlData = await rootBundle.loadString('assets/index.html'); + String htmlData = await rootBundle.loadString( + 'packages/talkjs/assets/index.html'); Uri uri = Uri.dataFromString(htmlData, mimeType: 'text/html', encoding: Encoding.getByName('utf-8')); webViewController.loadUrl(uri.toString()); diff --git a/talkjs/pubspec.yaml b/talkjs/pubspec.yaml index 0ca6140..c442e77 100644 --- a/talkjs/pubspec.yaml +++ b/talkjs/pubspec.yaml @@ -25,7 +25,7 @@ dev_dependencies: # The following section is specific to Flutter. flutter: assets: - - assets/index.html + - packages/talkjs/assets/index.html # To add assets to your package, add an assets section, like this: # assets: From 614a9334381cf867c150e3883ce4e62f87c4dbf7 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 19 May 2021 16:38:10 +0300 Subject: [PATCH 22/52] Cleanup Signed-off-by: Victor Omondi --- talkjs/lib/src/conversation.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/talkjs/lib/src/conversation.dart b/talkjs/lib/src/conversation.dart index d3eb61d..bd5c353 100644 --- a/talkjs/lib/src/conversation.dart +++ b/talkjs/lib/src/conversation.dart @@ -30,8 +30,7 @@ class ConversationBuilder { final userName = session.getUserName(user); final settings = participantSettings ?? {}; session.execute( - '$variableName.setParticipant($userName, ${json.encode(settings)});' - 'console.log("$userName: ${user.id}");'); + '$variableName.setParticipant($userName, ${json.encode(settings)});'); } } From b6b01cfca728cafb06436eb062e45e6d39672598 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Thu, 20 May 2021 16:08:43 +0300 Subject: [PATCH 23/52] Add Library Documentation Signed-off-by: Victor Omondi --- talkjs/lib/src/chatoptions.dart | 68 +++++++++++++++++++++++++++++--- talkjs/lib/src/conversation.dart | 38 ++++++++++++++++++ talkjs/lib/src/session.dart | 53 +++++++++++++++++++++++++ talkjs/lib/src/ui.dart | 29 +++++++++++--- talkjs/lib/src/user.dart | 32 +++++++++++++++ talkjs/lib/src/webview.dart | 1 + talkjs/lib/talkjs.dart | 7 ++++ 7 files changed, 217 insertions(+), 11 deletions(-) diff --git a/talkjs/lib/src/chatoptions.dart b/talkjs/lib/src/chatoptions.dart index 007637f..388ff36 100644 --- a/talkjs/lib/src/chatoptions.dart +++ b/talkjs/lib/src/chatoptions.dart @@ -1,6 +1,10 @@ +import 'package:talkjs/src/ui.dart'; + +/// The possible values for the Chat modes enum ChatMode { subject, participants } extension ChatModeString on ChatMode { + /// Converts this enum's values to String. String getValue() { late String result; switch (this) { @@ -16,9 +20,16 @@ extension ChatModeString on ChatMode { } } -enum TextDirection { rtl, ltr } +/// The values that dictate the chat direction. +enum TextDirection { + /// right-to-left + rtl, + /// left-to-right + ltr +} extension TextDirectionString on TextDirection { + /// Converts this enum's values to String. String getValue() { late String result; switch (this) { @@ -34,11 +45,35 @@ extension TextDirectionString on TextDirection { } } +/// 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. 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. bool enterSendsMessage; + /// The text displayed in the message field when the user hasn't started + /// typing anything. String? placeholder; + + /// This enables spell checking. + /// + /// Note that setting this to true may also enable autocorrect on some mobile + /// devices. + /// Defaults to false bool spellcheck; MessageFieldOptions({this.autofocus = true, this.enterSendsMessage = true, @@ -62,16 +97,34 @@ class MessageFieldOptions { } } -abstract class ChatOptions { +/// This class represents the various configuration options used to finetune the +/// behaviour of UI elements. +abstract class _ChatOptions { + /// Controls what text appears in the chat subtitle, right below the chat title. + /// + /// Defaults to [ChatMode.subject]. ChatMode chatSubtitleMode; + + /// Controls what text appears in the chat title, in the header above the messages. + /// + /// Defaults to [ChatMode.participants]. ChatMode chatTitleMode; + /// Controls the text direction (for supporting right-to-left languages such + /// as Arabic and Hebrew). + /// + /// Defaults to [TextDirection.rtl]. TextDirection dir; + + /// Used to control if the Chat Header is displayed in the UI. + /// + /// Defaults to true. bool showChatHeader; + /// Settings that affect the behavior of the message field MessageFieldOptions? messageField; - ChatOptions({this.chatSubtitleMode = ChatMode.subject, + _ChatOptions({this.chatSubtitleMode = ChatMode.subject, this.chatTitleMode = ChatMode.participants, this.dir = TextDirection.rtl, this.showChatHeader = true, this.messageField }); @@ -85,7 +138,8 @@ abstract class ChatOptions { }; } -class ChatBoxOptions extends ChatOptions{ +/// Options to configure the behaviour of the [ChatBox] UI. +class ChatBoxOptions extends _ChatOptions{ ChatBoxOptions({chatSubtitleMode, chatTitleMode, dir, showChatHeader, messageField}) : super(chatSubtitleMode: chatSubtitleMode, chatTitleMode: chatTitleMode, @@ -93,7 +147,8 @@ class ChatBoxOptions extends ChatOptions{ messageField: messageField); } -class InboxOptions extends ChatOptions { +/// Options to configure the behaviour of the [Inbox]. +class InboxOptions extends _ChatOptions { InboxOptions({chatSubtitleMode, chatTitleMode, dir, showChatHeader, messageField}) : super(chatSubtitleMode: chatSubtitleMode, chatTitleMode: chatTitleMode, @@ -101,7 +156,8 @@ class InboxOptions extends ChatOptions { messageField: messageField); } -class PopupOptions extends ChatOptions { +/// Options to configure the behaviour of the [Popup]. +class PopupOptions extends _ChatOptions { bool keepOpen; PopupOptions({this.keepOpen = false, chatSubtitleMode, chatTitleMode, dir, diff --git a/talkjs/lib/src/conversation.dart b/talkjs/lib/src/conversation.dart index bd5c353..cacf450 100644 --- a/talkjs/lib/src/conversation.dart +++ b/talkjs/lib/src/conversation.dart @@ -3,29 +3,49 @@ import 'dart:convert'; import './session.dart'; import './user.dart'; +/// 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 ConversationBuilder { + /// Custom metadata for this conversation Map? custom; + + /// Messages sent at the beginning of a chat. + /// + /// The messages will appear as system messages. List? welcomeMessages; + /// The URL to a photo which will be shown as the photo for the conversation. String? photoUrl; + + /// The conversation subject which will be displayed in the chat header. String? subject; + /// The current active TalkJS session. Session session; + + /// The JavaScript variable name for this object. String variableName; ConversationBuilder({required this.session, required this.variableName, this.custom, this.welcomeMessages, this.photoUrl, this.subject, }); + /// Sends a text message in a given conversation. void sendMessage(String text, MessageOptions options) { session.execute( '$variableName.sendMessage("$text", ${json.encode(options)});'); } + /// Used to set certain attributes for a specific conversation void setAttributes(ConversationAttributes attributes) { session.execute('$variableName.setAttributes(${json.encode(attributes)});'); } + /// Sets a participant of the conversation. void setParticipant(User user, {ParticipantSettings? participantSettings}) { final userName = session.getUserName(user); final settings = participantSettings ?? {}; @@ -35,6 +55,9 @@ class ConversationBuilder { } class MessageOptions { + /// Custom data that you may wish to associate with a message. + /// + /// The custom data is sent back to you via webhooks and the REST API. Map? custom; MessageOptions({this.custom}); @@ -44,11 +67,21 @@ class MessageOptions { }; } +/// Conversation attributes that can be set using +/// [ConversationBuilder.setAttributes] class ConversationAttributes { + /// Custom metadata for a conversation. Map? custom; + + /// Messages sent at the beginning of a chat. + /// + /// The messages will appear as system messages. List? welcomeMessages; + /// The URL to a photo which will be shown as the photo for the conversation. String? photoUrl; + + /// The conversation subject which will be displayed in the chat header. String? subject; ConversationAttributes({this.custom, this.welcomeMessages, this.photoUrl, @@ -63,9 +96,11 @@ class ConversationAttributes { }; } +/// Possible values for participants' permissions enum Access { read, readWrite } extension StringConversion on Access { + /// Converts this enum's values to String. String getValue() { late String result; switch (this) { @@ -81,7 +116,10 @@ extension StringConversion on Access { } class ParticipantSettings { + /// Specifies the participant's access permission for a conversation. final Access? access; + + /// Specifies the participants's notification settings. final bool? notify; const ParticipantSettings({this.access, this.notify}); diff --git a/talkjs/lib/src/session.dart b/talkjs/lib/src/session.dart index 71ae518..1137419 100644 --- a/talkjs/lib/src/session.dart +++ b/talkjs/lib/src/session.dart @@ -5,6 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import '../talkjs.dart'; + import './chatoptions.dart'; import './conversation.dart'; import './ui.dart'; @@ -13,16 +15,33 @@ import './webview.dart'; const chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'; +/// A session represents a currently active user. class Session { + /// Your TalkJS AppId that can be found your TalkJS [dashboard](https://talkjs.com/dashboard). String appId; + + /// The TalkJS [User] associated with the current user in your application. User me; + + /// The widget for showing the various chat UI elements. late Widget chatUI; + /// 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. String? signature; + /// List of JavaScript statements that haven't been executed. final List _pending = []; + + /// Used to control the underlying WebView WebViewController? _webViewController; + /// A mapping of user ids to the variable name of the respective JavaScript + /// Talk.User object. Map _users = {}; Session({required this.appId, required this.me, this.signature}) { @@ -61,6 +80,8 @@ class Session { } } + /// Returns the JavaScript variable name of the Talk.User object associated + /// with the given [User] String getUserName(User user) { if (_users[user.id] == null) { // Generate random variable name @@ -76,6 +97,7 @@ class Session { return _users[user.id]!; } + /// Evaluates the JavaScript statement given. void execute(String statement) { final controller = this._webViewController; if (controller != null) { @@ -85,14 +107,36 @@ class Session { } } + /// Disconnects all websockets, removes all UIs, and invalidates this session + /// + /// You cannot use any objects that were created in this session after you + /// destroy it. If you want to use TalkJS after having called [destroy()] + /// you must instantiate a new [Session] instance. void destroy() => execute('session.destroy();'); + /// Fetches an existing conversation or creates a new one. + /// + /// The [conversationId] is a unique identifier for this conversation, + /// such as a channel name or topic ID. Any user with access to this ID can + /// join this conversation. + /// + /// [Read about how to choose a good conversation ID for your use case.](https://talkjs.com/docs/Reference/Concepts/Conversations.html) + /// If you want to make a simple one-on-one conversation, consider using + /// [Talk.oneOnOneId] to generate one. + /// + /// Returns a [ConversationBuilder] that encapsulates a conversation between + /// me (given in the constructor) and zero or more other participants. ConversationBuilder getOrCreateConversation(String conversationId) { execute( 'const conversation = session.getOrCreateConversation("$conversationId")'); return ConversationBuilder(session: this, variableName: 'conversation'); } + /// Creates a [ChatBox] UI which shows a single conversation, without means to + /// switch between conversations. + /// + /// Call [createChatbox] on any page you want to show a [ChatBox] of a single + /// conversation. ChatBox createChatbox( ConversationBuilder selectedConversation, {ChatBoxOptions? chatBoxOptions}) { @@ -103,6 +147,11 @@ class Session { return ChatBox(session: this, variableName: 'chatBox'); } + /// Creates an [Inbox] which aside from providing a conversation UI, it can + /// also show a user's other converations and switch between them. + /// + /// You typically want to call the [Inbox.mount] method after creating the + /// [Inbox] to retrive the Widget needed to make it visible on your app. Inbox createInbox({InboxOptions? inboxOptions}) { final options = inboxOptions ?? {}; execute('const inbox = session.createInbox(${json.encode(options)});'); @@ -110,6 +159,10 @@ class Session { return Inbox(session: this, variableName: 'inbox'); } + /// Creates a [Popup] which is a well positioned box containing a conversation. + /// + /// It shows a single conversation, without means to switch between + /// conversations. Popup createPopup( ConversationBuilder conversation, {PopupOptions? popupOptions}) { final options = popupOptions ?? {}; diff --git a/talkjs/lib/src/ui.dart b/talkjs/lib/src/ui.dart index 9a323ef..0c39837 100644 --- a/talkjs/lib/src/ui.dart +++ b/talkjs/lib/src/ui.dart @@ -2,16 +2,23 @@ import 'package:flutter/material.dart'; import './session.dart'; -abstract class UI { +/// This class represents the various UI elements that TalkJS supports and the +/// methods common to all. +abstract class _UI { + /// The current active TalkJS session. Session session; + + /// The JavaScript variable name for this object. String variableName; - UI({required this.session, required this.variableName}); + _UI({required this.session, required this.variableName}); + /// Destroys this UI element and removes all event listeners it has running. void destroy() { session.execute('$variableName.destroy();'); } + /// Renders the UI and returns the Widget containing it. Widget mount() { session.execute( '$variableName.mount(document.getElementById("talkjs-container"));'); @@ -19,17 +26,29 @@ abstract class UI { } } -class ChatBox extends UI { +/// 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 _UI { ChatBox({session, variableName}) : super(session: session, variableName: variableName); } -class Inbox extends UI { +/// The main messaging UI component of TalkJS. +/// +/// It shows a user's conversation history and it allows them to write messages. +/// Create an Inbox through [Session.createInbox] and then call [mount] to show it. +class Inbox extends _UI { Inbox({session, variableName}) : super(session: session, variableName: variableName); } -class Popup extends UI { +/// A messaging UI for just a single conversation. +/// +/// Create a Popup through [Session.createPopup] and then call [mount] to show it. +/// There is no way for the user to switch between conversations +class Popup extends _UI { Popup({session, variableName}) : super(session: session, variableName: variableName); } \ No newline at end of file diff --git a/talkjs/lib/src/user.dart b/talkjs/lib/src/user.dart index 07f9b6c..bc73c88 100644 --- a/talkjs/lib/src/user.dart +++ b/talkjs/lib/src/user.dart @@ -1,17 +1,49 @@ +/// 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 User { + /// 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. String? availabilityText; + + /// Custom metadata for this user. 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. 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. List? phone; + /// The unique user identifier. String id; + + /// This user's name which will be displayed on the TalkJS UI String name; + /// The language on the UI. + /// + /// This field expects an [IETF language tag](https://www.w3.org/International/articles/language-tags/). String? locale; + + /// An optional URL to a photo which will be displayed as this user's avatar String? photoUrl; + /// This user's role which allows you to change the behaviour of TalkJS for + /// different users. String? role; + + /// The default message a user sees when starting a chat with this person. String? welcomeMessage; User({required this.id, required this.name, this.email, this.phone, diff --git a/talkjs/lib/src/webview.dart b/talkjs/lib/src/webview.dart index 6a0a1fe..2485247 100644 --- a/talkjs/lib/src/webview.dart +++ b/talkjs/lib/src/webview.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; +/// Wrapper around the [WebView] widget. class ChatWebView extends StatefulWidget { final ChatWebViewState state; diff --git a/talkjs/lib/talkjs.dart b/talkjs/lib/talkjs.dart index 0496291..f124bed 100644 --- a/talkjs/lib/talkjs.dart +++ b/talkjs/lib/talkjs.dart @@ -10,7 +10,14 @@ export 'src/ui.dart'; export 'src/user.dart'; export 'src/webview.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(); From f8c6a1e05c46af09de04aac7193673992d2605f8 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Thu, 20 May 2021 16:11:50 +0300 Subject: [PATCH 24/52] Update library's README Signed-off-by: Victor Omondi --- talkjs/README.md | 91 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 9 deletions(-) diff --git a/talkjs/README.md b/talkjs/README.md index 361207f..b2ad7c8 100644 --- a/talkjs/README.md +++ b/talkjs/README.md @@ -1,14 +1,87 @@ -# talkjs +# TalkJS -A new Flutter package project. +Flutter SDK for [TalkJS](https://talkjs.com) + +## Requirements + +- Dart sdk: ">=2.12.0 <3.0.0" +- Flutter: ">=1.17.0" +- Android: `minSDKVersion 19` + +## Installation + +Assumption: You have an existing Flutter Project. You can follow this [guide](https://flutter.dev/docs/get-started/test-drive#create-app) +on how to create a new Flutter Project. + +First, clone this repository on your computer. +```sh +git clone https://github.com/talkjs/flutter-sdk-victor.git +``` + +To add the package as a dependency, edit the dependencies section of +your project's **pubspec.yaml** file in your Flutter project as follows: + +```yaml +dependencies: + talkjs: + path: {path to directory containing this repository}/flutter-sdk-victor/talkjs +``` + +The path specified should be an absolute path from the root directory of +your system. + +Run the command: ```flutter pub get``` on the command line or through +Android Studio's **Get dependencies** button. ## Getting Started -This project is a starting point for a Dart -[package](https://flutter.dev/developing-packages/), -a library module containing code that can be shared easily across -multiple Flutter or Dart projects. +If you used the [guide](https://flutter.dev/docs/get-started/test-drive#create-app) +mentioned above to create a new Flutter project, replace everything in +**lib/main.dart** with the following code: + +```dart +import 'package:flutter/material.dart'; +import 'package:talkjs/talkjs.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'TalkJS Demo', + home: Scaffold( + body: initChat() + ) + ); + } + + Widget initChat() { + final me = User(id: '123456', name: 'Alice'); + final other = User(id: '654321', name: 'Sebastian'); + + final session = Session(appId: 'YOUR_APP_ID', me: me); + final conversation = session.getOrCreateConversation( + Talk.oneOnOneId(me.id, other.id) + ); + + conversation.setParticipant(me); + conversation.setParticipant(other); + + final chatBox = session.createChatbox(conversation); + return chatBox.mount(); + } +} +``` + +Replace ```YOUR_APP_ID``` with the App ID on the TalkJS [Dashboard](https://talkjs.com/dashboard/login.) + +For users with an existing Flutter project, you can use the ```Widget``` returned +by the ```initChat``` function in the example above as part of an existing +```Widget``` definition or a navigation route + +## Documentation -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +The SDK API reference can be found in **doc/api**. \ No newline at end of file From 34c2f7f94c03c9dbcc87b8c99e8aade7adeb0200 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Thu, 20 May 2021 16:12:33 +0300 Subject: [PATCH 25/52] Update pubspec.yaml Added some basic metadata about the library. Signed-off-by: Victor Omondi --- talkjs/pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/talkjs/pubspec.yaml b/talkjs/pubspec.yaml index c442e77..af71d1f 100644 --- a/talkjs/pubspec.yaml +++ b/talkjs/pubspec.yaml @@ -1,8 +1,8 @@ name: talkjs -description: A new Flutter package project. +description: SDK for TalkJS version: 0.0.1 author: -homepage: +homepage: https://talkjs.com environment: sdk: ">=2.12.0 <3.0.0" From 56bfafb60fa11ca9b51652655eba71f713915b6d Mon Sep 17 00:00:00 2001 From: Franco Bugnano Date: Wed, 15 Dec 2021 16:23:14 +0100 Subject: [PATCH 26/52] Started ChatBox refactoring --- talkjs/README.md | 10 +- talkjs/lib/assets/index.html | 11 +- talkjs/lib/src/chatoptions.dart | 199 ++++++++++++++++++++++++++------ talkjs/lib/src/session.dart | 38 ++++-- talkjs/lib/src/ui.dart | 10 +- talkjs/lib/src/webview.dart | 8 +- 6 files changed, 220 insertions(+), 56 deletions(-) diff --git a/talkjs/README.md b/talkjs/README.md index b2ad7c8..b70123d 100644 --- a/talkjs/README.md +++ b/talkjs/README.md @@ -18,7 +18,7 @@ First, clone this repository on your computer. git clone https://github.com/talkjs/flutter-sdk-victor.git ``` -To add the package as a dependency, edit the dependencies section of +To add the package as a dependency, edit the dependencies section of your project's **pubspec.yaml** file in your Flutter project as follows: ```yaml @@ -33,6 +33,12 @@ your system. Run the command: ```flutter pub get``` on the command line or through Android Studio's **Get dependencies** button. +To debug on Android, put this line in the `android/app/src/main/AndroidManifest.xml` as a property of the ` - + @@ -29,4 +32,4 @@
- \ No newline at end of file + diff --git a/talkjs/lib/src/chatoptions.dart b/talkjs/lib/src/chatoptions.dart index 388ff36..76ff671 100644 --- a/talkjs/lib/src/chatoptions.dart +++ b/talkjs/lib/src/chatoptions.dart @@ -1,4 +1,5 @@ -import 'package:talkjs/src/ui.dart'; +import './ui.dart'; +import './conversation.dart'; /// The possible values for the Chat modes enum ChatMode { subject, participants } @@ -55,7 +56,7 @@ class MessageFieldOptions { /// effects. /// If you need more control, consider setting [autofocus] to false and /// calling focus() at appropriate times. - bool autofocus; // Convert to "smart" + bool? autofocus; // Convert to "smart" /// If set to true, pressing the enter key sends the message /// (if there is text in the message field). @@ -63,7 +64,7 @@ class MessageFieldOptions { /// When set to false, the only way to send a message is by clicking or /// touching the "Send" button. /// Defaults to true. - bool enterSendsMessage; + bool? enterSendsMessage; /// The text displayed in the message field when the user hasn't started /// typing anything. @@ -74,77 +75,199 @@ class MessageFieldOptions { /// Note that setting this to true may also enable autocorrect on some mobile /// devices. /// Defaults to false - bool spellcheck; + bool? spellcheck; - MessageFieldOptions({this.autofocus = true, this.enterSendsMessage = true, - this.placeholder, this.spellcheck = false - }); + /// TODO: visible + + MessageFieldOptions({this.autofocus, this.enterSendsMessage, this.placeholder, this.spellcheck}); Map toJson() { - final result = { - 'enterSendsMessage': enterSendsMessage, - 'placeholder': placeholder, - 'spellcheck': spellcheck - }; + final Map result = {}; + + if (autofocus != null) { + if (autofocus == true) { + result['autofocus'] = 'smart'; + } else { + result['autofocus'] = autofocus; + } + } - 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; } } +/// The possible values for showTranslationToggle +enum TranslationToggle { off, on, auto } + +/// The possible values for translateConversations +enum TranslateConversations { off, on, auto } + /// This class represents the various configuration options used to finetune the /// behaviour of UI elements. abstract class _ChatOptions { /// Controls what text appears in the chat subtitle, right below the chat title. /// /// Defaults to [ChatMode.subject]. - ChatMode chatSubtitleMode; + ChatMode? chatSubtitleMode; /// Controls what text appears in the chat title, in the header above the messages. /// /// Defaults to [ChatMode.participants]. - ChatMode chatTitleMode; + ChatMode? chatTitleMode; /// Controls the text direction (for supporting right-to-left languages such /// as Arabic and Hebrew). /// /// Defaults to [TextDirection.rtl]. - TextDirection dir; + TextDirection? dir; + + /// Settings that affect the behavior of the message field + MessageFieldOptions? messageField; + + /// TODO: messageFilter /// Used to control if the Chat Header is displayed in the UI. /// /// Defaults to true. - bool showChatHeader; + bool? showChatHeader; - /// Settings that affect the behavior of the message field - MessageFieldOptions? messageField; + /// 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. + TranslationToggle? showTranslationToggle; + + /// Overrides the theme used for this chat UI. + String? theme; + + /// TODO: thirdparties + + /// Enables conversation translation with Google Translate. + TranslateConversations? translateConversations; - _ChatOptions({this.chatSubtitleMode = ChatMode.subject, - this.chatTitleMode = ChatMode.participants, this.dir = TextDirection.rtl, - this.showChatHeader = true, this.messageField + /// This option specifies which conversations should be translated in this UI. + List? conversationsToTranslate; + + /// This option specifies which conversation Ids should be translated in this UI. + List? conversationIdsToTranslate; + + _ChatOptions({this.chatSubtitleMode, + this.chatTitleMode, + this.dir, + this.messageField, + this.showChatHeader, + this.showTranslationToggle, + this.theme, + this.translateConversations, + this.conversationsToTranslate, + this.conversationIdsToTranslate, }); - Map toJson() => { - 'chatSubtitleMode': chatSubtitleMode.getValue(), - 'chatTitleMode': chatTitleMode.getValue(), - 'dir': dir.getValue(), - 'showChatHeader': showChatHeader, - 'messageField': messageField ?? {} - }; + Map toJson() { + final Map result = {}; + + if (chatSubtitleMode != null) { + result['chatSubtitleMode'] = chatSubtitleMode!.getValue(); + } + + if (chatTitleMode != null) { + result['chatTitleMode'] = chatTitleMode!.getValue(); + } + + if (dir != null) { + result['dir'] = dir!.getValue(); + } + + if (messageField != null) { + result['messageField'] = messageField; + } + + if (showChatHeader != null) { + result['showChatHeader'] = showChatHeader; + } + + // 'auto' gets the priority over the boolean value + if (showTranslationToggle != null) { + switch (showTranslationToggle) { + case TranslationToggle.off: + result['showTranslationToggle'] = false; + break; + case TranslationToggle.on: + result['showTranslationToggle'] = true; + break; + case TranslationToggle.auto: + result['showTranslationToggle'] = 'auto'; + break; + } + } + + if (theme != null) { + result['theme'] = theme; + } + + if (conversationsToTranslate != null) { + // Highest priority: TranslateConversations.off + if (translateConversations != TranslateConversations.off) { + // High priority: conversationsToTranslate + // TODO -- This does not work yet, as it results in a string value enclosed by double quotes + result['translateConversations'] ??= '[' + conversationsToTranslate + !.map((conversation) => conversation.variableName) + .join(', ') + + ']'; + } + } + + if (conversationIdsToTranslate != null) { + // Highest priority: TranslateConversations.off + if (translateConversations != TranslateConversations.off) { + // Medium priority: conversationIdsToTranslate + result['translateConversations'] ??= conversationIdsToTranslate; + } + } + + // Low priority: translateConversations + if (translateConversations != null) { + result['translateConversations'] ??= translateConversations; + } + + return result; + } } /// Options to configure the behaviour of the [ChatBox] UI. class ChatBoxOptions extends _ChatOptions{ - ChatBoxOptions({chatSubtitleMode, chatTitleMode, dir, showChatHeader, - messageField}) - : super(chatSubtitleMode: chatSubtitleMode, chatTitleMode: chatTitleMode, - dir: dir, showChatHeader: showChatHeader, - messageField: messageField); + ChatBoxOptions({chatSubtitleMode, + chatTitleMode, + dir, + messageField, + showChatHeader, + showTranslationToggle, + theme, + translateConversations, + conversationsToTranslate, + conversationIdsToTranslate, + }) + : super(chatSubtitleMode: chatSubtitleMode, + chatTitleMode: chatTitleMode, + dir: dir, + messageField: messageField, + showChatHeader: showChatHeader, + showTranslationToggle: showTranslationToggle, + theme: theme, + translateConversations: translateConversations, + conversationsToTranslate: conversationsToTranslate, + conversationIdsToTranslate: conversationIdsToTranslate, + ); } /// Options to configure the behaviour of the [Inbox]. @@ -165,4 +288,4 @@ class PopupOptions extends _ChatOptions { : super(chatSubtitleMode: chatSubtitleMode, chatTitleMode: chatTitleMode, dir: dir, showChatHeader: showChatHeader, messageField: messageField); -} \ No newline at end of file +} diff --git a/talkjs/lib/src/session.dart b/talkjs/lib/src/session.dart index 1137419..42e4f27 100644 --- a/talkjs/lib/src/session.dart +++ b/talkjs/lib/src/session.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import 'package:flutter/foundation.dart' show kDebugMode; import '../talkjs.dart'; @@ -100,6 +101,11 @@ class Session { /// Evaluates the JavaScript statement given. void execute(String statement) { final controller = this._webViewController; + + if (kDebugMode) { + print('📘 WebView DEBUG: $statement'); + } + if (controller != null) { controller.evaluateJavascript(statement); } else { @@ -137,12 +143,30 @@ class Session { /// /// Call [createChatbox] on any page you want to show a [ChatBox] of a single /// conversation. - ChatBox createChatbox( - ConversationBuilder selectedConversation, - {ChatBoxOptions? chatBoxOptions}) { - final options = chatBoxOptions ?? {}; - execute('const chatBox = session.createChatbox(' - '${selectedConversation.variableName}, ${json.encode(options)});'); + ChatBox createChatbox({ChatMode? chatSubtitleMode, + ChatMode? chatTitleMode, + TextDirection? dir, + MessageFieldOptions? messageField, + bool? showChatHeader, + TranslationToggle? showTranslationToggle, + String? theme, + TranslateConversations? translateConversations, + List? conversationsToTranslate, + List? conversationIdsToTranslate, + }) { + final options = ChatBoxOptions(chatSubtitleMode: chatSubtitleMode, + chatTitleMode: chatTitleMode, + dir: dir, + messageField: messageField, + showChatHeader: showChatHeader, + showTranslationToggle: showTranslationToggle, + theme: theme, + translateConversations: translateConversations, + conversationsToTranslate: conversationsToTranslate, + conversationIdsToTranslate: conversationIdsToTranslate, + ); + + execute('const chatBox = session.createChatbox(${json.encode(options)});'); return ChatBox(session: this, variableName: 'chatBox'); } @@ -173,4 +197,4 @@ class Session { return Popup(session: this, variableName: variableName); } -} \ No newline at end of file +} diff --git a/talkjs/lib/src/ui.dart b/talkjs/lib/src/ui.dart index 0c39837..e2ed78d 100644 --- a/talkjs/lib/src/ui.dart +++ b/talkjs/lib/src/ui.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import './session.dart'; +import './conversation.dart'; /// This class represents the various UI elements that TalkJS supports and the /// methods common to all. @@ -18,10 +19,13 @@ abstract class _UI { session.execute('$variableName.destroy();'); } + void select(ConversationBuilder conversation /* TODO: params */) { + session.execute('$variableName.select(${conversation.variableName});'); + } + /// Renders the UI and returns the Widget containing it. Widget mount() { - session.execute( - '$variableName.mount(document.getElementById("talkjs-container"));'); + session.execute('$variableName.mount(document.getElementById("talkjs-container"));'); return session.chatUI; } } @@ -51,4 +55,4 @@ class Inbox extends _UI { class Popup extends _UI { Popup({session, variableName}) : super(session: session, variableName: variableName); -} \ No newline at end of file +} diff --git a/talkjs/lib/src/webview.dart b/talkjs/lib/src/webview.dart index 2485247..0e9f457 100644 --- a/talkjs/lib/src/webview.dart +++ b/talkjs/lib/src/webview.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import 'package:flutter/foundation.dart' show kReleaseMode; /// Wrapper around the [WebView] widget. class ChatWebView extends StatefulWidget { @@ -20,11 +21,14 @@ class ChatWebViewState extends State { ChatWebViewState(WebViewCreatedCallback webViewFn, PageFinishedCallback jsFn) { // Enable hybrid composition. - if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView(); + if (Platform.isAndroid) { + WebView.platform = SurfaceAndroidWebView(); + } this.webView = WebView( initialUrl: '', javascriptMode: JavascriptMode.unrestricted, + debuggingEnabled: !kReleaseMode, onWebViewCreated: webViewFn, onPageFinished: jsFn, ); @@ -39,4 +43,4 @@ class ChatWebViewState extends State { Widget build(BuildContext context) { return this.webView; } -} \ No newline at end of file +} From 1a62499e24c67186c259316a351fff78ff90590b Mon Sep 17 00:00:00 2001 From: Franco Bugnano Date: Fri, 17 Dec 2021 15:36:35 +0100 Subject: [PATCH 27/52] completed user and session refactoring --- talkjs/lib/src/chatoptions.dart | 92 +++++++++++++++++++++------------ talkjs/lib/src/session.dart | 82 ++++++++++++++++++++--------- talkjs/lib/src/user.dart | 76 +++++++++++++++++++++------ talkjs/lib/src/webview.dart | 2 +- 4 files changed, 179 insertions(+), 73 deletions(-) diff --git a/talkjs/lib/src/chatoptions.dart b/talkjs/lib/src/chatoptions.dart index 76ff671..1b5596f 100644 --- a/talkjs/lib/src/chatoptions.dart +++ b/talkjs/lib/src/chatoptions.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import './ui.dart'; import './conversation.dart'; @@ -7,17 +9,12 @@ enum ChatMode { subject, participants } extension ChatModeString on ChatMode { /// Converts this enum's values to String. String getValue() { - late String result; switch (this) { case ChatMode.participants: - result = 'participants'; - break; + return 'participants'; case ChatMode.subject: - result = 'subject'; - break; + return 'subject'; } - - return result; } } @@ -32,17 +29,12 @@ enum TextDirection { extension TextDirectionString on TextDirection { /// Converts this enum's values to String. String getValue() { - late String result; switch (this) { case TextDirection.rtl: - result = 'rtl'; - break; + return 'rtl'; case TextDirection.ltr: - result = 'ltr'; - break; + return 'ltr'; } - - return result; } } @@ -82,7 +74,7 @@ class MessageFieldOptions { MessageFieldOptions({this.autofocus, this.enterSendsMessage, this.placeholder, this.spellcheck}); Map toJson() { - final Map result = {}; + final result = {}; if (autofocus != null) { if (autofocus == true) { @@ -111,9 +103,37 @@ class MessageFieldOptions { /// 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'; + } + } +} + /// This class represents the various configuration options used to finetune the /// behaviour of UI elements. abstract class _ChatOptions { @@ -173,8 +193,15 @@ abstract class _ChatOptions { this.conversationIdsToTranslate, }); - Map toJson() { - final Map result = {}; + /// 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() { + final result = {}; if (chatSubtitleMode != null) { result['chatSubtitleMode'] = chatSubtitleMode!.getValue(); @@ -198,17 +225,7 @@ abstract class _ChatOptions { // 'auto' gets the priority over the boolean value if (showTranslationToggle != null) { - switch (showTranslationToggle) { - case TranslationToggle.off: - result['showTranslationToggle'] = false; - break; - case TranslationToggle.on: - result['showTranslationToggle'] = true; - break; - case TranslationToggle.auto: - result['showTranslationToggle'] = 'auto'; - break; - } + result['showTranslationToggle'] = showTranslationToggle!.getValue(); } if (theme != null) { @@ -219,10 +236,10 @@ abstract class _ChatOptions { // Highest priority: TranslateConversations.off if (translateConversations != TranslateConversations.off) { // High priority: conversationsToTranslate - // TODO -- This does not work yet, as it results in a string value enclosed by double quotes + // This results in a string value that will be parsed later result['translateConversations'] ??= '[' + conversationsToTranslate !.map((conversation) => conversation.variableName) - .join(', ') + .join(',') + ']'; } } @@ -237,10 +254,21 @@ abstract class _ChatOptions { // Low priority: translateConversations if (translateConversations != null) { - result['translateConversations'] ??= translateConversations; + result['translateConversations'] ??= translateConversations!.getValue(); } - return result; + final jsonString = json.encode(result); + + // Evil black magic that fixes the fact that the JSON value for the + // translateConversations property is a string, but we need + // its value without the surrounding " characters, which would not be + // valid JSON anymore. + // This removes the " characters if a string value starts with [ and ends + // with ], so that it becomes a list of identifiers. + return jsonString.replaceAllMapped( + RegExp(r'":"(\[[^"\]]*])"'), + (Match m) => '":${m[1]}' + ); } } diff --git a/talkjs/lib/src/session.dart b/talkjs/lib/src/session.dart index 42e4f27..0eab6ce 100644 --- a/talkjs/lib/src/session.dart +++ b/talkjs/lib/src/session.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -14,8 +13,6 @@ import './ui.dart'; import './user.dart'; import './webview.dart'; -const chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'; - /// A session represents a currently active user. class Session { /// Your TalkJS AppId that can be found your TalkJS [dashboard](https://talkjs.com/dashboard). @@ -45,20 +42,26 @@ class Session { /// Talk.User object. Map _users = {}; + // A counter to ensure that IDs are unique + int _idCounter = 0; + Session({required this.appId, required this.me, this.signature}) { this.chatUI = ChatWebView(_webViewCreatedCallback, _onPageFinished); // Initialize Session object - final options = {'appId': appId}; - execute('const options = ${json.encode(options)};'); + final options = {}; - final variableName = getUserName(this.me); - execute('options["me"] = $variableName;'); + options['appId'] = appId; if (signature != null) { - execute('options["signature"] = "$signature";'); + options["signature"] = signature; } + execute('const options = ${json.encode(options)};'); + + final variableName = getUserName(this.me); + execute('options["me"] = $variableName;'); + execute('const session = new Talk.Session(options);'); } @@ -74,6 +77,17 @@ class Session { void _onPageFinished(String url) { 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('📗 WebView DEBUG: $js'); + } + + this._webViewController!.evaluateJavascript(js); + // Execute any pending instructions for (var statement in this._pending) { this._webViewController!.evaluateJavascript(statement); @@ -81,17 +95,25 @@ class Session { } } + /// 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'; + } + /// Returns the JavaScript variable name of the Talk.User object associated /// with the given [User] String getUserName(User user) { if (_users[user.id] == null) { - // Generate random variable name - final rand = Random(); - final characters = List.generate( - 15, (index) => chars[rand.nextInt(chars.length)]); - final variableName = characters.join(); + // Generate unique variable name + final variableName = 'user${getUniqueId()}'; - execute('const $variableName = new Talk.User(${json.encode(me)});'); + execute('const $variableName = new Talk.User(${user.getJsonString()});'); _users[user.id] = variableName; } @@ -133,9 +155,12 @@ class Session { /// Returns a [ConversationBuilder] that encapsulates a conversation between /// me (given in the constructor) and zero or more other participants. ConversationBuilder getOrCreateConversation(String conversationId) { - execute( - 'const conversation = session.getOrCreateConversation("$conversationId")'); - return ConversationBuilder(session: this, variableName: 'conversation'); + // Generate unique variable name + final variableName = 'conversation${getUniqueId()}'; + + execute('const $variableName = session.getOrCreateConversation("$conversationId")'); + + return ConversationBuilder(session: this, variableName: variableName); } /// Creates a [ChatBox] UI which shows a single conversation, without means to @@ -166,9 +191,11 @@ class Session { conversationIdsToTranslate: conversationIdsToTranslate, ); - execute('const chatBox = session.createChatbox(${json.encode(options)});'); + final variableName = 'chatBox${getUniqueId()}'; + + execute('const $variableName = session.createChatbox(${options.getJsonString()});'); - return ChatBox(session: this, variableName: 'chatBox'); + return ChatBox(session: this, variableName: variableName); } /// Creates an [Inbox] which aside from providing a conversation UI, it can @@ -177,10 +204,13 @@ class Session { /// You typically want to call the [Inbox.mount] method after creating the /// [Inbox] to retrive the Widget needed to make it visible on your app. Inbox createInbox({InboxOptions? inboxOptions}) { - final options = inboxOptions ?? {}; - execute('const inbox = session.createInbox(${json.encode(options)});'); + final options = inboxOptions!; // TODO: change this to match the ChatBox + + final variableName = 'inbox${getUniqueId()}'; - return Inbox(session: this, variableName: 'inbox'); + execute('const $variableName = session.createInbox(${options.getJsonString()});'); + + return Inbox(session: this, variableName: variableName); } /// Creates a [Popup] which is a well positioned box containing a conversation. @@ -189,12 +219,14 @@ class Session { /// conversations. Popup createPopup( ConversationBuilder conversation, {PopupOptions? popupOptions}) { - final options = popupOptions ?? {}; - final variableName = 'popup'; + final options = popupOptions!; // TODO: change this to match the ChatBox + + final variableName = 'popup${getUniqueId()}'; execute('const $variableName = session.createPopup(' - '${conversation.variableName}, ${json.encode(options)});'); + '${conversation.variableName}, ${options.getJsonString()});'); return Popup(session: this, variableName: variableName); } } + diff --git a/talkjs/lib/src/user.dart b/talkjs/lib/src/user.dart index bc73c88..fc904b0 100644 --- a/talkjs/lib/src/user.dart +++ b/talkjs/lib/src/user.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + /// A user of your app. /// /// TalkJS uses the [id] to uniquely identify this user. All other fields of a @@ -46,21 +48,65 @@ class User { /// The default message a user sees when starting a chat with this person. String? welcomeMessage; + // To support creating users with only an id + bool _idOnly; + User({required this.id, required this.name, this.email, this.phone, this.availabilityText, this.locale, this.photoUrl, this.role, this.custom, this.welcomeMessage - }); - - Map toJson() => { - 'id': id, - 'name': name, - 'email': email, - 'phone': phone, - 'availabilityText': availabilityText, - 'locale': locale, - 'photoUrl': photoUrl, - 'role': role, - 'welcomeMessage': welcomeMessage, - 'custom': custom - }; -} \ No newline at end of file + }) : this._idOnly = false; + + User.fromId(this.id) : this.name = '', this._idOnly = true; + + /// 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); + } + } +} diff --git a/talkjs/lib/src/webview.dart b/talkjs/lib/src/webview.dart index 0e9f457..2bf815b 100644 --- a/talkjs/lib/src/webview.dart +++ b/talkjs/lib/src/webview.dart @@ -26,7 +26,7 @@ class ChatWebViewState extends State { } this.webView = WebView( - initialUrl: '', + initialUrl: 'about:blank', javascriptMode: JavascriptMode.unrestricted, debuggingEnabled: !kReleaseMode, onWebViewCreated: webViewFn, From 7dfc4f8532c2e9fe6b4e156572ff511ae6f0a0d2 Mon Sep 17 00:00:00 2001 From: Franco Bugnano Date: Mon, 20 Dec 2021 10:32:51 +0100 Subject: [PATCH 28/52] refactored ConversationBuilder --- talkjs/lib/src/conversation.dart | 138 ++++++++++++------------------- talkjs/lib/src/session.dart | 4 +- 2 files changed, 57 insertions(+), 85 deletions(-) diff --git a/talkjs/lib/src/conversation.dart b/talkjs/lib/src/conversation.dart index cacf450..409aee1 100644 --- a/talkjs/lib/src/conversation.dart +++ b/talkjs/lib/src/conversation.dart @@ -3,6 +3,21 @@ import 'dart:convert'; 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'; + } + } +} + /// This represents a conversation that is about to be created, fetched, or /// updated. /// @@ -24,117 +39,74 @@ class ConversationBuilder { /// The conversation subject which will be displayed in the chat header. String? subject; + /// For internal use only. Implementation detail that may change anytime. + /// /// The current active TalkJS session. Session session; + /// For internal use only. Implementation detail that may change anytime. + /// /// The JavaScript variable name for this object. String variableName; + /// Don't use the ConversationBuilder constructor directly. + // use session.getOrCreateConversation instead. ConversationBuilder({required this.session, required this.variableName, this.custom, this.welcomeMessages, this.photoUrl, this.subject, }); /// Sends a text message in a given conversation. - void sendMessage(String text, MessageOptions options) { - session.execute( - '$variableName.sendMessage("$text", ${json.encode(options)});'); - } + void sendMessage(String text, {Map? custom}) { + final result = {}; - /// Used to set certain attributes for a specific conversation - void setAttributes(ConversationAttributes attributes) { - session.execute('$variableName.setAttributes(${json.encode(attributes)});'); - } + if (custom != null) { + result['custom'] = custom; + } - /// Sets a participant of the conversation. - void setParticipant(User user, {ParticipantSettings? participantSettings}) { - final userName = session.getUserName(user); - final settings = participantSettings ?? {}; - session.execute( - '$variableName.setParticipant($userName, ${json.encode(settings)});'); + session.execute('$variableName.sendMessage("$text", ${json.encode(result)});'); } -} - -class MessageOptions { - /// Custom data that you may wish to associate with a message. - /// - /// The custom data is sent back to you via webhooks and the REST API. - Map? custom; - - MessageOptions({this.custom}); - Map toJson() => { - 'custom': custom ?? {} - }; -} - -/// Conversation attributes that can be set using -/// [ConversationBuilder.setAttributes] -class ConversationAttributes { - /// Custom metadata for a conversation. - Map? custom; - - /// Messages sent at the beginning of a chat. - /// - /// The messages will appear as system messages. - List? welcomeMessages; - - /// The URL to a photo which will be shown as the photo for the conversation. - String? photoUrl; - - /// The conversation subject which will be displayed in the chat header. - String? subject; - - ConversationAttributes({this.custom, this.welcomeMessages, this.photoUrl, - this.subject - }); - - Map toJson() => { - 'custom': custom, - 'welcomeMessages': welcomeMessages, - 'photoUrl': photoUrl, - 'subject': subject - }; -} + /// Used to set certain attributes for a specific conversation + void setAttributes({Map? custom, List? welcomeMessages, String? photoUrl, String? subject}) { + final result = {}; -/// Possible values for participants' permissions -enum Access { read, readWrite } + if (custom != null) { + result['custom'] = custom; + this.custom = custom; + } -extension StringConversion on Access { - /// Converts this enum's values to String. - String getValue() { - late String result; - switch (this) { - case Access.read: - result = 'Read'; - break; - case Access.readWrite: - result = 'ReadWrite'; - break; + if (welcomeMessages != null) { + result['welcomeMessages'] = welcomeMessages; + this.welcomeMessages = welcomeMessages; } - return result; - } -} -class ParticipantSettings { - /// Specifies the participant's access permission for a conversation. - final Access? access; + if (photoUrl != null) { + result['photoUrl'] = photoUrl; + this.photoUrl = photoUrl; + } - /// Specifies the participants's notification settings. - final bool? notify; + if (subject != null) { + result['subject'] = subject; + this.subject = subject; + } - const ParticipantSettings({this.access, this.notify}); + session.execute('$variableName.setAttributes(${json.encode(result)});'); + } - Map toJson() { - final Map result = {}; + /// Sets a participant of the conversation. + void setParticipant(User user, {ParticipantAccess? access, bool? notify}) { + final userVariableName = session.getUserVariableName(user); + final result = {}; if (access != null) { - result['access'] = access!.getValue(); + result['access'] = access.getValue(); } if (notify != null) { result['notify'] = notify; } - return result; + session.execute('$variableName.setParticipant($userVariableName, ${json.encode(result)});'); } -} \ No newline at end of file +} + diff --git a/talkjs/lib/src/session.dart b/talkjs/lib/src/session.dart index 0eab6ce..7e7b1fb 100644 --- a/talkjs/lib/src/session.dart +++ b/talkjs/lib/src/session.dart @@ -59,7 +59,7 @@ class Session { execute('const options = ${json.encode(options)};'); - final variableName = getUserName(this.me); + final variableName = getUserVariableName(this.me); execute('options["me"] = $variableName;'); execute('const session = new Talk.Session(options);'); @@ -108,7 +108,7 @@ class Session { /// Returns the JavaScript variable name of the Talk.User object associated /// with the given [User] - String getUserName(User user) { + String getUserVariableName(User user) { if (_users[user.id] == null) { // Generate unique variable name final variableName = 'user${getUniqueId()}'; From 23a9facb882dadd89569920a8d00de58fc2ec40a Mon Sep 17 00:00:00 2001 From: Franco Bugnano Date: Mon, 20 Dec 2021 15:47:21 +0100 Subject: [PATCH 29/52] started refactoring of the ChatBox --- talkjs/lib/assets/index.html | 16 ++------- talkjs/lib/src/session.dart | 16 --------- talkjs/lib/src/ui.dart | 69 ++++++++++++++++++++++++++++++------ talkjs/pubspec.yaml | 2 +- 4 files changed, 61 insertions(+), 42 deletions(-) diff --git a/talkjs/lib/assets/index.html b/talkjs/lib/assets/index.html index 98ce30a..b522341 100644 --- a/talkjs/lib/assets/index.html +++ b/talkjs/lib/assets/index.html @@ -4,23 +4,11 @@ + (function(t,a,l,k,j,s){ + s=a.createElement('script');s.async=1;s.src="https://cdn.talkjs.com/talk.js";a.head.appendChild(s) + ;k=t.Promise;t.Talk={v:3,ready:{then:function(f){if(k)return new k(function(r,e){l.push([f,r,e])});l + .push([f])},catch:function(){return k&&new k()},c:l}};})(window,document,[]); +
diff --git a/lib/src/chatbox.dart b/lib/src/chatbox.dart index b23fac9..42bd5d2 100644 --- a/lib/src/chatbox.dart +++ b/lib/src/chatbox.dart @@ -6,16 +6,12 @@ import 'package:flutter/foundation.dart'; import 'package:webview_flutter/webview_flutter.dart'; -import 'package:provider/provider.dart'; - import './session.dart'; import './conversation.dart'; import './chatoptions.dart'; import './user.dart'; import './message.dart'; -typedef BlurHandler = void Function(); -typedef FocusHandler = void Function(); typedef SendMessageHandler = void Function(SendMessageEvent event); typedef TranslationToggledHandler = void Function(TranslationToggledEvent event); @@ -44,43 +40,35 @@ class TranslationToggledEvent { /// 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 ChatMode? chatSubtitleMode; - final ChatMode? chatTitleMode; + final Session session; + final TextDirection? dir; final MessageFieldOptions? messageField; final bool? showChatHeader; final TranslationToggle? showTranslationToggle; final String? theme; final TranslateConversations? translateConversations; - final List? conversationsToTranslate; - final List? conversationIdsToTranslate; final Conversation? conversation; final bool? asGuest; - final BlurHandler? onBlur; - final FocusHandler? onFocus; final SendMessageHandler? onSendMessage; final TranslationToggledHandler? onTranslationToggled; - const ChatBox({Key? key, - this.chatSubtitleMode, - this.chatTitleMode, - this.dir, - this.messageField, - this.showChatHeader, - this.showTranslationToggle, - this.theme, - this.translateConversations, - this.conversationsToTranslate, - this.conversationIdsToTranslate, - this.conversation, - this.asGuest, - this.onBlur, - this.onFocus, - this.onSendMessage, - this.onTranslationToggled, - }) : super(key: key); + const ChatBox({ + Key? key, + required this.session, + this.dir, + this.messageField, + this.showChatHeader, + this.showTranslationToggle, + this.theme, + this.translateConversations, + this.conversation, + this.asGuest, + this.onSendMessage, + this.onTranslationToggled, + }) : super(key: key); @override State createState() => ChatBoxState(); @@ -122,15 +110,13 @@ class ChatBoxState extends State { print('📗 chatbox.build (_webViewCreated: $_webViewCreated)'); } - final sessionState = context.read(); - if (!_webViewCreated) { // If it's the first time that the widget is built, then build everything _webViewCreated = true; execute('let chatBox;'); - _createSession(sessionState); + _createSession(); _createChatBox(); _createConversation(); @@ -162,26 +148,24 @@ class ChatBoxState extends State { onWebViewCreated: _webViewCreatedCallback, onPageFinished: _onPageFinished, javascriptChannels: { - JavascriptChannel(name: 'JSCBlur', onMessageReceived: _jscBlur), - JavascriptChannel(name: 'JSCFocus', onMessageReceived: _jscFocus), JavascriptChannel(name: 'JSCSendMessage', onMessageReceived: _jscSendMessage), JavascriptChannel(name: 'JSCTranslationToggled', onMessageReceived: _jscTranslationToggled), }); } - void _createSession(SessionState sessionState) { + void _createSession() { // Initialize Session object final options = {}; - options['appId'] = sessionState.appId; + options['appId'] = widget.session.appId; - if (sessionState.signature != null) { - options["signature"] = sessionState.signature; + if (widget.session.signature != null) { + options["signature"] = widget.session.signature; } execute('const options = ${json.encode(options)};'); - final variableName = getUserVariableName(sessionState.me); + final variableName = getUserVariableName(widget.session.me); execute('options["me"] = $variableName;'); execute('const session = new Talk.Session(options);'); @@ -189,38 +173,28 @@ class ChatBoxState extends State { void _createChatBox() { _oldOptions = ChatBoxOptions( - chatSubtitleMode: widget.chatSubtitleMode, - chatTitleMode: widget.chatTitleMode, dir: widget.dir, messageField: widget.messageField, showChatHeader: widget.showChatHeader, showTranslationToggle: widget.showTranslationToggle, theme: widget.theme, translateConversations: widget.translateConversations, - conversationsToTranslate: widget.conversationsToTranslate, - conversationIdsToTranslate: widget.conversationIdsToTranslate, ); execute('chatBox = session.createChatbox(${_oldOptions!.getJsonString(this)});'); - execute('chatBox.on("blur", (event) => JSCBlur.postMessage(JSON.stringify(event)));'); - execute('chatBox.on("focus", (event) => JSCFocus.postMessage(JSON.stringify(event)));'); 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( - chatSubtitleMode: widget.chatSubtitleMode, - chatTitleMode: widget.chatTitleMode, dir: widget.dir, messageField: widget.messageField, showChatHeader: widget.showChatHeader, showTranslationToggle: widget.showTranslationToggle, theme: widget.theme, translateConversations: widget.translateConversations, - conversationsToTranslate: widget.conversationsToTranslate, - conversationIdsToTranslate: widget.conversationIdsToTranslate, ); if (options != _oldOptions) { @@ -245,8 +219,11 @@ class ChatBoxState extends State { if (_oldConversation != null) { execute('chatBox.select(${getConversationVariableName(_oldConversation!)}, ${json.encode(result)});'); } else { - // TODO: null or undefined? - execute('chatBox.select(null, ${json.encode(result)});'); + if (result.isNotEmpty) { + execute('chatBox.select(undefined, ${json.encode(result)});'); + } else { + execute('chatBox.select(undefined);'); + } } } @@ -300,22 +277,6 @@ class ChatBoxState extends State { } } - void _jscBlur(JavascriptMessage message) { - if (kDebugMode) { - print('📗 chatbox._jscBlur: ${message.message}'); - } - - widget.onBlur?.call(); - } - - void _jscFocus(JavascriptMessage message) { - if (kDebugMode) { - print('📗 chatbox._jscFocus: ${message.message}'); - } - - widget.onFocus?.call(); - } - void _jscSendMessage(JavascriptMessage message) { if (kDebugMode) { print('📗 chatbox._jscSendMessage: ${message.message}'); diff --git a/lib/src/chatoptions.dart b/lib/src/chatoptions.dart index 163551a..214f552 100644 --- a/lib/src/chatoptions.dart +++ b/lib/src/chatoptions.dart @@ -1,32 +1,14 @@ import 'dart:convert'; import 'dart:ui'; -import 'package:flutter/foundation.dart'; - import './chatbox.dart'; -import './conversation.dart'; - -/// The possible values for the Chat modes -enum ChatMode { subject, participants } - -extension ChatModeString on ChatMode { - /// Converts this enum's values to String. - String getValue() { - switch (this) { - case ChatMode.participants: - return 'participants'; - case ChatMode.subject: - return 'subject'; - } - } -} /// The values that dictate the chat direction. enum TextDirection { /// right-to-left rtl, /// left-to-right - ltr + ltr, } extension TextDirectionString on TextDirection { @@ -51,7 +33,7 @@ class MessageFieldOptions { /// effects. /// If you need more control, consider setting [autofocus] to false and /// calling focus() at appropriate times. - bool? autofocus; // Convert to "smart" + 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). @@ -59,22 +41,22 @@ class MessageFieldOptions { /// When set to false, the only way to send a message is by clicking or /// touching the "Send" button. /// Defaults to true. - bool? enterSendsMessage; + final bool? enterSendsMessage; /// The text displayed in the message field when the user hasn't started /// typing anything. - String? placeholder; + final String? placeholder; /// This enables spell checking. /// /// Note that setting this to true may also enable autocorrect on some mobile /// devices. /// Defaults to false - bool? spellcheck; + final bool? spellcheck; /// TODO: visible - MessageFieldOptions({this.autofocus, this.enterSendsMessage, this.placeholder, this.spellcheck}); + const MessageFieldOptions({this.autofocus, this.enterSendsMessage, this.placeholder, this.spellcheck}); Map toJson() { final result = {}; @@ -169,60 +151,41 @@ extension TranslateConversationsValue on TranslateConversations { /// Options to configure the behaviour of the [ChatBox] UI. class ChatBoxOptions { - /// Controls what text appears in the chat subtitle, right below the chat title. - /// - /// Defaults to [ChatMode.subject]. - ChatMode? chatSubtitleMode; - - /// Controls what text appears in the chat title, in the header above the messages. - /// - /// Defaults to [ChatMode.participants]. - ChatMode? chatTitleMode; - /// Controls the text direction (for supporting right-to-left languages such /// as Arabic and Hebrew). /// /// Defaults to [TextDirection.rtl]. - TextDirection? dir; + final TextDirection? dir; /// Settings that affect the behavior of the message field - MessageFieldOptions? messageField; + final MessageFieldOptions? messageField; /// TODO: messageFilter /// Used to control if the Chat Header is displayed in the UI. /// /// Defaults to true. - bool? showChatHeader; + 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. - TranslationToggle? showTranslationToggle; + final TranslationToggle? showTranslationToggle; /// Overrides the theme used for this chat UI. - String? theme; + final String? theme; /// TODO: thirdparties /// Enables conversation translation with Google Translate. - TranslateConversations? translateConversations; - - /// This option specifies which conversations should be translated in this UI. - List? conversationsToTranslate; - - /// This option specifies which conversation Ids should be translated in this UI. - List? conversationIdsToTranslate; + final TranslateConversations? translateConversations; - ChatBoxOptions({this.chatSubtitleMode, - this.chatTitleMode, + const ChatBoxOptions({ this.dir, this.messageField, this.showChatHeader, this.showTranslationToggle, this.theme, this.translateConversations, - this.conversationsToTranslate, - this.conversationIdsToTranslate, }); /// For internal use only. Implementation detail that may change anytime. @@ -235,14 +198,6 @@ class ChatBoxOptions { String getJsonString(ChatBoxState chatBox) { final result = {}; - if (chatSubtitleMode != null) { - result['chatSubtitleMode'] = chatSubtitleMode!.getValue(); - } - - if (chatTitleMode != null) { - result['chatTitleMode'] = chatTitleMode!.getValue(); - } - if (dir != null) { result['dir'] = dir!.getValue(); } @@ -264,43 +219,11 @@ class ChatBoxOptions { result['theme'] = theme; } - if (conversationsToTranslate != null) { - // Highest priority: TranslateConversations.off - if (translateConversations != TranslateConversations.off) { - // High priority: conversationsToTranslate - // This results in a string value that will be parsed later - result['translateConversations'] ??= '[' + conversationsToTranslate - !.map((conversation) => chatBox.getConversationVariableName(conversation)) - .join(',') - + ']'; - } - } - - if (conversationIdsToTranslate != null) { - // Highest priority: TranslateConversations.off - if (translateConversations != TranslateConversations.off) { - // Medium priority: conversationIdsToTranslate - result['translateConversations'] ??= conversationIdsToTranslate; - } - } - - // Low priority: translateConversations if (translateConversations != null) { - result['translateConversations'] ??= translateConversations!.getValue(); + result['translateConversations'] = translateConversations!.getValue(); } - final jsonString = json.encode(result); - - // Evil black magic that fixes the fact that the JSON value for the - // translateConversations property is a string, but we need - // its value without the surrounding " characters, which would not be - // valid JSON anymore. - // This removes the " characters if a string value starts with [ and ends - // with ], so that it becomes a list of identifiers. - return jsonString.replaceAllMapped( - RegExp(r'":"(\[[^"\]]*])"'), - (Match m) => '":${m[1]}' - ); + return json.encode(result); } bool operator ==(Object other) { @@ -312,14 +235,6 @@ class ChatBoxOptions { return false; } - if (chatSubtitleMode != other.chatSubtitleMode) { - return false; - } - - if (chatTitleMode != other.chatTitleMode) { - return false; - } - if (dir != other.dir) { return false; } @@ -344,28 +259,16 @@ class ChatBoxOptions { return false; } - if (!listEquals(conversationsToTranslate, other.conversationsToTranslate)) { - return false; - } - - if (!listEquals(conversationIdsToTranslate, other.conversationIdsToTranslate)) { - return false; - } - return true; } int get hashCode => hashValues( - chatSubtitleMode, - chatTitleMode, dir, messageField, showChatHeader, showTranslationToggle, theme, translateConversations, - conversationsToTranslate, - conversationIdsToTranslate, ); } diff --git a/lib/src/conversation.dart b/lib/src/conversation.dart index ff8a2b6..76c7cf7 100644 --- a/lib/src/conversation.dart +++ b/lib/src/conversation.dart @@ -22,13 +22,13 @@ extension ParticipantAccessString on ParticipantAccess { // Participants are users + options relative to this conversation class Participant { - User user; + final User user; - ParticipantAccess? access; + final ParticipantAccess? access; - bool? notify; + final bool? notify; - Participant(this.user, {this.access, this.notify}); + const Participant(this.user, {this.access, this.notify}); Participant.of(Participant other) : user = User.of(other.user), @@ -70,23 +70,23 @@ class Participant { /// Instead, instantiate a TalkJS UI using methods such as [Session.createInbox]. class _BaseConversation { /// The unique conversation identifier. - String id; + final String id; /// Custom metadata for this conversation - Map? custom; + final Map? custom; /// Messages sent at the beginning of a chat. /// /// The messages will appear as system messages. - List? welcomeMessages; + final List? welcomeMessages; /// The URL to a photo which will be shown as the photo for the conversation. - String? photoUrl; + final String? photoUrl; /// The conversation subject which will be displayed in the chat header. - String? subject; + final String? subject; - _BaseConversation({ + const _BaseConversation({ required this.id, this.custom, this.welcomeMessages, @@ -97,19 +97,32 @@ class _BaseConversation { class Conversation extends _BaseConversation { // The participants for this conversation - Set participants; + final Set participants; - Conversation({ + // 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, - this.participants = const {}, - }) : super(id: id, custom: custom, welcomeMessages: welcomeMessages, photoUrl: photoUrl, subject: subject); + required this.participants, + }) + : _session = session, + super( + id: id, + custom: custom, + welcomeMessages: welcomeMessages, + photoUrl: photoUrl, + subject: subject, + ); Conversation.of(Conversation other) - : participants = Set.of(other.participants.map((participant) => Participant.of(participant))), + : _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, @@ -140,6 +153,10 @@ class Conversation extends _BaseConversation { return false; } + if (_session != other._session) { + return false; + } + if (!setEquals(participants, other.participants)) { return false; } @@ -167,7 +184,7 @@ class Conversation extends _BaseConversation { return true; } - int get hashCode => hashValues(participants, id, custom, welcomeMessages, photoUrl, subject); + int get hashCode => hashValues(_session, participants, id, custom, welcomeMessages, photoUrl, subject); } class ConversationData extends _BaseConversation { diff --git a/lib/src/conversationlist.dart b/lib/src/conversationlist.dart index b22c928..4c7cdfe 100644 --- a/lib/src/conversationlist.dart +++ b/lib/src/conversationlist.dart @@ -6,12 +6,9 @@ import 'package:flutter/foundation.dart' show kDebugMode; import 'package:webview_flutter/webview_flutter.dart'; -import 'package:provider/provider.dart'; - import './session.dart'; import './conversation.dart'; import './user.dart'; -import './chatbox.dart'; typedef SelectConversationHandler = void Function(SelectConversationEvent event); @@ -26,36 +23,10 @@ class SelectConversationEvent { others = json['others'].map((user) => UserData.fromJson(user)).toList(); } -/// The possible values for the Chat modes -enum ConversationTitleMode { subject, participants, auto } - -extension ConversationTitleModeString on ConversationTitleMode { - /// Converts this enum's values to String. - String getValue() { - switch (this) { - case ConversationTitleMode.participants: - return 'participants'; - case ConversationTitleMode.subject: - return 'subject'; - case ConversationTitleMode.auto: - return 'auto'; - } - } -} - class ConversationListOptions { /// Controls if the feed header containing the toggle to enable desktop notifications is shown. /// Defaults to true. - bool? showFeedHeader; - - /// Controls how a chat is displayed in the feed of chats. - /// - /// Note: when set to `"subject"` but a conversation has no subject set, then - /// TalkJS falls back to `"participants"`. - /// - /// When not set, defaults to `"auto"`, which means that in group conversations - /// that have a subject set, the subject is displayed and otherwise the participants. - ConversationTitleMode? feedConversationTitleMode; + 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 @@ -81,9 +52,9 @@ class ConversationListOptions { //bool? showMobileBackButton; /// Overrides the theme used for this chat UI. - String? theme; + final String? theme; - ConversationListOptions({this.showFeedHeader, this.feedConversationTitleMode, this.theme}); + const ConversationListOptions({this.showFeedHeader, this.theme}); /// For internal use only. Implementation detail that may change anytime. /// @@ -98,10 +69,6 @@ class ConversationListOptions { result['showFeedHeader'] = showFeedHeader; } - if (feedConversationTitleMode != null) { - result['feedConversationTitleMode'] = feedConversationTitleMode!.getValue(); - } - if (theme != null) { result['theme'] = theme; } @@ -111,23 +78,21 @@ class ConversationListOptions { } class ConversationList extends StatefulWidget { + final Session session; + final bool? showFeedHeader; - final ConversationTitleMode? feedConversationTitleMode; final String? theme; - final BlurHandler? onBlur; - final FocusHandler? onFocus; final SelectConversationHandler? onSelectConversation; - const ConversationList({Key? key, - this.showFeedHeader, - this.feedConversationTitleMode, - this.theme, - this.onBlur, - this.onFocus, - this.onSelectConversation, - }) : super(key: key); + const ConversationList({ + Key? key, + required this.session, + this.showFeedHeader, + this.theme, + this.onSelectConversation, + }) : super(key: key); @override State createState() => ConversationListState(); @@ -154,12 +119,10 @@ class ConversationListState extends State { print('📗 conversationlist.build (_webViewCreated: $_webViewCreated)'); } - final sessionState = context.read(); - if (!_webViewCreated) { _webViewCreated = true; - _createSession(sessionState); + _createSession(); _createConversationList(); execute('conversationList.mount(document.getElementById("talkjs-container"));'); @@ -172,25 +135,23 @@ class ConversationListState extends State { onWebViewCreated: _webViewCreatedCallback, onPageFinished: _onPageFinished, javascriptChannels: { - JavascriptChannel(name: 'JSCBlur', onMessageReceived: _jscBlur), - JavascriptChannel(name: 'JSCFocus', onMessageReceived: _jscFocus), JavascriptChannel(name: 'JSCSelectConversation', onMessageReceived: _jscSelectConversation), }); } - void _createSession(SessionState sessionState) { + void _createSession() { // Initialize Session object final options = {}; - options['appId'] = sessionState.appId; + options['appId'] = widget.session.appId; - if (sessionState.signature != null) { - options["signature"] = sessionState.signature; + if (widget.session.signature != null) { + options["signature"] = widget.session.signature; } execute('const options = ${json.encode(options)};'); - final variableName = getUserVariableName(sessionState.me); + final variableName = getUserVariableName(widget.session.me); execute('options["me"] = $variableName;'); execute('const session = new Talk.Session(options);'); @@ -199,15 +160,15 @@ class ConversationListState extends State { void _createConversationList() { final options = ConversationListOptions( showFeedHeader: widget.showFeedHeader, - feedConversationTitleMode: widget.feedConversationTitleMode, theme: widget.theme, ); execute('const conversationList = session.createInbox(${options.getJsonString(this)});'); - execute('conversationList.on("blur", (event) => JSCBlur.postMessage(JSON.stringify(event)));'); - execute('conversationList.on("focus", (event) => JSCFocus.postMessage(JSON.stringify(event)));'); - execute('conversationList.on("selectConversation", (event) => JSCSelectConversation.postMessage(JSON.stringify(event)));'); + execute('''conversationList.on("selectConversation", (event) => { + event.preventDefault(); + JSCSelectConversation.postMessage(JSON.stringify(event)); + }); '''); } void _webViewCreatedCallback(WebViewController webViewController) async { @@ -250,29 +211,11 @@ class ConversationListState extends State { } } - void _jscBlur(JavascriptMessage message) { - if (kDebugMode) { - print('📗 conversationlist._jscBlur: ${message.message}'); - } - - widget.onBlur?.call(); - } - - void _jscFocus(JavascriptMessage message) { - if (kDebugMode) { - print('📗 conversationlist._jscFocus: ${message.message}'); - } - - widget.onFocus?.call(); - } - void _jscSelectConversation(JavascriptMessage message) { if (kDebugMode) { print('📗 conversationlist._jscSelectConversation: ${message.message}'); } - execute('conversationList.select(null);'); - widget.onSelectConversation?.call(SelectConversationEvent.fromJson(json.decode(message.message))); } diff --git a/lib/src/message.dart b/lib/src/message.dart index d0611e0..6f4bce0 100644 --- a/lib/src/message.dart +++ b/lib/src/message.dart @@ -2,8 +2,8 @@ enum MessageType { UserMessage, SystemMessage } class Attachment { - String url; - int size; + final String url; + final int size; Attachment.fromJson(Map json) : url = json['url'], @@ -12,31 +12,31 @@ class Attachment { class SentMessage { /// The message ID of the message that was sent - String? id; + final String? id; /// The ID of the conversation that the message belongs to - String conversationId; + final String conversationId; /// Identifies the message as either a User message or System message - MessageType type; + final MessageType type; /// Contains an Array of User.id's that have read the message - List readBy; + final List readBy; /// Contains the user ID for the person that sent the message - String senderId; // redundant since the user is always me, but keeps it consistant + final String senderId; // redundant since the user is always me, but keeps it consistant /// Contains the message's text - String? 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. - Attachment? attachment; + 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] - List? location; + final List? location; SentMessage.fromJson(Map json) : id = json['id'], diff --git a/lib/src/session.dart b/lib/src/session.dart index c175e79..e8f78e0 100644 --- a/lib/src/session.dart +++ b/lib/src/session.dart @@ -1,16 +1,31 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - import './user.dart'; +import './conversation.dart'; /// A session represents a currently active user. -class Session extends StatelessWidget { +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. - final User me; + 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] /// @@ -20,45 +35,50 @@ class Session extends StatelessWidget { /// code. final String? signature; - /// The child widget - final Widget? child; - - Session({Key? key, required this.appId, required this.me, this.signature, this.child}) : super(key: key); - - @override - Widget build(BuildContext context) { - // Provide the model to all widgets within the app. We're using - // ChangeNotifierProvider because that's a simple way to rebuild - // widgets when a model changes. We could also just use - // Provider, but then we would have to listen to Counter ourselves. - // - // Read Provider's docs to learn about all the available providers. - return ChangeNotifierProvider( - // Initialize the model in the builder. That way, Provider - // can own Counter's lifecycle, making sure to call `dispose` - // when not needed anymore. - create: (context) => SessionState(appId: appId, me: me, signature: signature), - child: child, - ); - } -} - -/// Session state that is passed to child widgets -class SessionState with ChangeNotifier { - /// Your TalkJS AppId that can be found your TalkJS [dashboard](https://talkjs.com/dashboard). - String appId; + Session({required this.appId, this.signature}); - /// The TalkJS [User] associated with the current user in your application. - User me; + User getOrCreateUser({ + 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, + ); - /// 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. - String? signature; + User getUserById(String id) => User.fromId(id, this); - SessionState({required this.appId, required this.me, this.signature}); + Conversation getOrCreateConversation({ + 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 index 4239d4a..8148653 100644 --- a/lib/src/user.dart +++ b/lib/src/user.dart @@ -3,6 +3,8 @@ 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 @@ -51,9 +53,17 @@ class _BaseUser { /// 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 + const _BaseUser({ + required this.id, + required this.name, + this.email, + this.phone, + this.availabilityText, + this.locale, + this.photoUrl, + this.role, + this.custom, + this.welcomeMessage, }); } @@ -61,29 +71,54 @@ class User extends _BaseUser { // To support creating users with only an id final bool _idOnly; - const User({required String id, required String name, List? email, List? phone, - String? availabilityText, String? locale, String? photoUrl, String? role, Map? custom, - String? welcomeMessage - }) : _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) : _idOnly = true, super(id: id, name: ''); + // 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) - : _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, - ); - + : _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. /// @@ -146,6 +181,10 @@ class User extends _BaseUser { return false; } + if (_session != other._session) { + return false; + } + if (_idOnly != other._idOnly) { return false; } @@ -194,6 +233,7 @@ class User extends _BaseUser { } int get hashCode => hashValues( + _session, _idOnly, availabilityText, custom, @@ -211,14 +251,15 @@ class User extends _BaseUser { 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']); + 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/pubspec.lock b/pubspec.lock index 2f5ece0..fe45fb3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,13 +81,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" - nested: - dependency: transitive - description: - name: nested - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" path: dependency: transitive description: @@ -102,13 +95,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.2" - provider: - dependency: "direct main" - description: - name: provider - url: "https://pub.dartlang.org" - source: hosted - version: "6.0.2" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 8a20449..83f51cc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,6 @@ dependencies: webview_flutter: ^3.0.0 crypto: ^3.0.1 - provider: ^6.0.0 dev_dependencies: flutter_test: From 6ae7923913c4e40ed7a8761b9806e1a1da407bcb Mon Sep 17 00:00:00 2001 From: Franco Bugnano Date: Wed, 19 Jan 2022 16:28:14 +0100 Subject: [PATCH 39/52] Changed the getOrCreate functions to be simply called get --- lib/src/session.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/session.dart b/lib/src/session.dart index e8f78e0..dd299b8 100644 --- a/lib/src/session.dart +++ b/lib/src/session.dart @@ -37,7 +37,7 @@ class Session with ChangeNotifier { Session({required this.appId, this.signature}); - User getOrCreateUser({ + User getUser({ required String id, required String name, List? email, @@ -64,7 +64,7 @@ class Session with ChangeNotifier { User getUserById(String id) => User.fromId(id, this); - Conversation getOrCreateConversation({ + Conversation getConversation({ required String id, Map? custom, List? welcomeMessages, From 505b3e8e4c77c1cc4e7ec7e4702dc6f1c1c151ef Mon Sep 17 00:00:00 2001 From: Franco Bugnano Date: Mon, 24 Jan 2022 08:46:44 +0100 Subject: [PATCH 40/52] Fixed crash related to list hashing --- lib/src/conversation.dart | 11 ++++++++++- lib/src/user.dart | 7 ++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/src/conversation.dart b/lib/src/conversation.dart index 76c7cf7..cc82e6f 100644 --- a/lib/src/conversation.dart +++ b/lib/src/conversation.dart @@ -184,7 +184,16 @@ class Conversation extends _BaseConversation { return true; } - int get hashCode => hashValues(_session, participants, id, custom, welcomeMessages, photoUrl, subject); + 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 { diff --git a/lib/src/user.dart b/lib/src/user.dart index 8148653..80c5183 100644 --- a/lib/src/user.dart +++ b/lib/src/user.dart @@ -236,9 +236,10 @@ class User extends _BaseUser { _session, _idOnly, availabilityText, - custom, - email, - phone, + (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, From 8cc6b2953961a69acbff90d2c492c327f1817e25 Mon Sep 17 00:00:00 2001 From: Franco Bugnano Date: Mon, 24 Jan 2022 15:30:02 +0100 Subject: [PATCH 41/52] Implemented the highlightedWords property --- lib/src/chatbox.dart | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/src/chatbox.dart b/lib/src/chatbox.dart index 42bd5d2..78f2a8e 100644 --- a/lib/src/chatbox.dart +++ b/lib/src/chatbox.dart @@ -48,6 +48,7 @@ class ChatBox extends StatefulWidget { final TranslationToggle? showTranslationToggle; final String? theme; final TranslateConversations? translateConversations; + final List highlightedWords; final Conversation? conversation; final bool? asGuest; @@ -64,6 +65,7 @@ class ChatBox extends StatefulWidget { this.showTranslationToggle, this.theme, this.translateConversations, + this.highlightedWords = const [], this.conversation, this.asGuest, this.onSendMessage, @@ -101,6 +103,7 @@ class ChatBoxState extends State { /// Objects stored for comparing changes ChatBoxOptions? _oldOptions; + List _oldHighlightedWords = []; bool? _oldAsGuest; Conversation? _oldConversation; @@ -118,6 +121,7 @@ class ChatBoxState extends State { _createSession(); _createChatBox(); + _setHighlightedWords(); _createConversation(); execute('chatBox.mount(document.getElementById("talkjs-container"));'); @@ -130,8 +134,10 @@ class ChatBoxState extends State { final chatBoxRecreated = _checkRecreateChatBox(); if (chatBoxRecreated) { + _setHighlightedWords(); _createConversation(); } else { + _checkHighlightedWords(); _checkRecreateConversation(); } @@ -237,6 +243,22 @@ class ChatBoxState extends State { 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 _webViewCreatedCallback(WebViewController webViewController) async { if (kDebugMode) { print('📗 chatbox._webViewCreatedCallback'); From 3bf55221d60160859b0104883235455292022c03 Mon Sep 17 00:00:00 2001 From: Franco Bugnano Date: Wed, 26 Jan 2022 16:22:04 +0100 Subject: [PATCH 42/52] Started working on ConversationPredicate and MessagePredicate --- lib/src/chatbox.dart | 4 +- lib/src/predicate.dart | 222 +++++++++++++++++++++++++++++++++++++++++ lib/talkjs.dart | 1 + 3 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 lib/src/predicate.dart diff --git a/lib/src/chatbox.dart b/lib/src/chatbox.dart index 78f2a8e..87822c1 100644 --- a/lib/src/chatbox.dart +++ b/lib/src/chatbox.dart @@ -48,7 +48,7 @@ class ChatBox extends StatefulWidget { final TranslationToggle? showTranslationToggle; final String? theme; final TranslateConversations? translateConversations; - final List highlightedWords; + final List highlightedWords = const []; final Conversation? conversation; final bool? asGuest; @@ -65,7 +65,7 @@ class ChatBox extends StatefulWidget { this.showTranslationToggle, this.theme, this.translateConversations, - this.highlightedWords = const [], + //this.highlightedWords = const [], // Commented out due to bug #1953 this.conversation, this.asGuest, this.onSendMessage, diff --git a/lib/src/predicate.dart b/lib/src/predicate.dart new file mode 100644 index 0000000..61ac6cd --- /dev/null +++ b/lib/src/predicate.dart @@ -0,0 +1,222 @@ +import 'dart:convert'; + +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(); + + @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; + } +} + +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(''); + + @override + String toString() { + if (_exists == null) { + return super.toString(); + } else if (_exists!) { + return 'exists'; + } else { + return '!exists'; + } + } + + @override + dynamic toJson() { + if (_exists == null) { + return super.toJson(); + } else if (_exists!) { + return 'exists'; + } else { + return '!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}); + + @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; + } +} + +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}); + + @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; + } +} + +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}); + + @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; + } +} + diff --git a/lib/talkjs.dart b/lib/talkjs.dart index ce0a58c..14d0a6e 100644 --- a/lib/talkjs.dart +++ b/lib/talkjs.dart @@ -9,6 +9,7 @@ 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 { From a76575f26ec28532ee3a5dbb82022d7b2c3c110f Mon Sep 17 00:00:00 2001 From: Franco Bugnano Date: Fri, 28 Jan 2022 15:29:32 +0100 Subject: [PATCH 43/52] Implemented messageFilter for the ChatBox and feedFilter for the ConversationList --- lib/src/chatbox.dart | 66 +++++------ lib/src/chatoptions.dart | 2 + lib/src/conversation.dart | 8 +- lib/src/conversationlist.dart | 52 +++++++-- lib/src/message.dart | 6 +- lib/src/predicate.dart | 195 +++++++++++++++++++++++++++++++++ lib/src/user.dart | 12 +- test/talkjs_test.dart | 199 ++++++++++++++++++++++++++++++++++ 8 files changed, 486 insertions(+), 54 deletions(-) diff --git a/lib/src/chatbox.dart b/lib/src/chatbox.dart index 87822c1..c5d2fdb 100644 --- a/lib/src/chatbox.dart +++ b/lib/src/chatbox.dart @@ -11,6 +11,7 @@ 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); @@ -49,6 +50,7 @@ class ChatBox extends StatefulWidget { final String? theme; final TranslateConversations? translateConversations; final List highlightedWords = const []; + final MessagePredicate messageFilter; final Conversation? conversation; final bool? asGuest; @@ -66,6 +68,7 @@ class ChatBox extends StatefulWidget { this.theme, this.translateConversations, //this.highlightedWords = const [], // Commented out due to bug #1953 + this.messageFilter = const MessagePredicate(), this.conversation, this.asGuest, this.onSendMessage, @@ -104,6 +107,7 @@ class ChatBoxState extends State { /// Objects stored for comparing changes ChatBoxOptions? _oldOptions; List _oldHighlightedWords = []; + MessagePredicate _oldMessageFilter = const MessagePredicate(); bool? _oldAsGuest; Conversation? _oldConversation; @@ -121,7 +125,7 @@ class ChatBoxState extends State { _createSession(); _createChatBox(); - _setHighlightedWords(); + // messageFilter and highlightedWords are set as options for the chatbox _createConversation(); execute('chatBox.mount(document.getElementById("talkjs-container"));'); @@ -134,9 +138,10 @@ class ChatBoxState extends State { final chatBoxRecreated = _checkRecreateChatBox(); if (chatBoxRecreated) { - _setHighlightedWords(); + // messageFilter and highlightedWords are set as options for the chatbox _createConversation(); } else { + _checkMessageFilter(); _checkHighlightedWords(); _checkRecreateConversation(); } @@ -187,6 +192,9 @@ class ChatBoxState extends State { 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)));'); @@ -259,6 +267,22 @@ class ChatBoxState extends State { 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'); @@ -420,6 +444,15 @@ class ChatBoxState extends State { } } + /// 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. @@ -438,32 +471,3 @@ class ChatBoxState extends State { } } -/// Encapsulates the message entry field tied to the currently selected conversation. -class MessageField { - /// The ChatBox associated with this message field - ChatBoxState chatbox; - - /// The JavaScript variable name for this object. - String variableName; - - MessageField({required this.chatbox, required this.variableName}); - - /// Focuses the message entry field. - /// - /// Note that on mobile devices, this will cause the on-screen keyboard to pop up, obscuring part - /// of the screen. - void focus() { - chatbox.execute('$variableName.focus();'); - } - - /// Sets the message field to `text`. - /// - /// Useful if you want to guide your user with message suggestions. If you want to start a UI - /// with a given text showing immediately, call this method before calling Inbox.mount - void setText(String text) { - chatbox.execute('$variableName.setText("$text");'); - } - - /// TODO: setVisible(visible: boolean | ConversationPredicate): void; -} - diff --git a/lib/src/chatoptions.dart b/lib/src/chatoptions.dart index 214f552..cf6a7b8 100644 --- a/lib/src/chatoptions.dart +++ b/lib/src/chatoptions.dart @@ -223,6 +223,8 @@ class ChatBoxOptions { result['translateConversations'] = translateConversations!.getValue(); } + chatBox.setExtraOptions(result); + return json.encode(result); } diff --git a/lib/src/conversation.dart b/lib/src/conversation.dart index cc82e6f..39787df 100644 --- a/lib/src/conversation.dart +++ b/lib/src/conversation.dart @@ -125,8 +125,8 @@ class Conversation extends _BaseConversation { 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, + custom: (other.custom != null ? Map.of(other.custom!) : null), + welcomeMessages: (other.welcomeMessages != null ? List.of(other.welcomeMessages!) : null), photoUrl: other.photoUrl, subject: other.subject ); @@ -199,8 +199,8 @@ class Conversation extends _BaseConversation { 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, + 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 index 4c7cdfe..94fccc9 100644 --- a/lib/src/conversationlist.dart +++ b/lib/src/conversationlist.dart @@ -9,6 +9,7 @@ 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); @@ -36,16 +37,6 @@ class ConversationListOptions { /// NOT NEEDED FOR FLUTTER? //bool? useBrowserHistory; - /// Used to control which conversations are shown in the conversation feed, depending on access - /// level, custom conversation attributes or message read status. - /// - /// See ConversationPredicate for all available options. - /// - /// You can also modify the filter on the fly using {@link Inbox.setFeedFilter}. - /// - /// TODO: NOT YET IMPLEMENTED FOR FLUTTER - //ConversationPredicate? feedFilter; - /// Whether to show a "Back" button at the top of the chat screen on mobile devices. /// /// NOT NEEDED FOR FLUTTER? @@ -73,6 +64,8 @@ class ConversationListOptions { result['theme'] = theme; } + conversationList.setExtraOptions(result); + return json.encode(result); } } @@ -84,6 +77,8 @@ class ConversationList extends StatefulWidget { final String? theme; + final ConversationPredicate feedFilter; + final SelectConversationHandler? onSelectConversation; const ConversationList({ @@ -91,6 +86,7 @@ class ConversationList extends StatefulWidget { required this.session, this.showFeedHeader, this.theme, + this.feedFilter = const ConversationPredicate(), this.onSelectConversation, }) : super(key: key); @@ -113,6 +109,9 @@ class ConversationListState extends State { /// Talk.User object. final _users = {}; + /// Objects stored for comparing changes + ConversationPredicate _oldFeedFilter = const ConversationPredicate(); + @override Widget build(BuildContext context) { if (kDebugMode) { @@ -124,8 +123,15 @@ class ConversationListState extends State { _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( @@ -163,6 +169,8 @@ class ConversationListState extends State { theme: widget.theme, ); + _oldFeedFilter = ConversationPredicate.of(widget.feedFilter); + execute('const conversationList = session.createInbox(${options.getJsonString(this)});'); execute('''conversationList.on("selectConversation", (event) => { @@ -171,6 +179,22 @@ class ConversationListState extends State { }); '''); } + 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'); @@ -246,6 +270,14 @@ class ConversationListState extends State { 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. diff --git a/lib/src/message.dart b/lib/src/message.dart index 6f4bce0..57df158 100644 --- a/lib/src/message.dart +++ b/lib/src/message.dart @@ -41,11 +41,11 @@ class SentMessage { SentMessage.fromJson(Map json) : id = json['id'], conversationId = json['conversationId'], - type = json['type'] == 'UserMessage' ? MessageType.UserMessage : MessageType.SystemMessage, + 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; + 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 index 61ac6cd..225ffe6 100644 --- a/lib/src/predicate.dart +++ b/lib/src/predicate.dart @@ -1,4 +1,7 @@ import 'dart:convert'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; class FieldPredicate { final String _operand; @@ -10,6 +13,11 @@ class FieldPredicate { 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); @@ -30,6 +38,36 @@ class FieldPredicate { 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 { @@ -42,6 +80,8 @@ class CustomFieldPredicate extends FieldPredicate { CustomFieldPredicate.exists() : _exists = true, super.equals(''); CustomFieldPredicate.notExists() : _exists = false, super.notEquals(''); + CustomFieldPredicate.of(CustomFieldPredicate other) : _exists = other._exists, super.of(other); + @override String toString() { if (_exists == null) { @@ -63,6 +103,41 @@ class CustomFieldPredicate extends FieldPredicate { 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 { @@ -90,6 +165,11 @@ class ConversationPredicate { 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); @@ -112,6 +192,37 @@ class ConversationPredicate { 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 { @@ -148,6 +259,12 @@ class SenderPredicate { 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); @@ -174,6 +291,42 @@ class SenderPredicate { 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 { @@ -192,6 +345,12 @@ class MessagePredicate { 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); @@ -218,5 +377,41 @@ class MessagePredicate { 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/user.dart b/lib/src/user.dart index 80c5183..76532e9 100644 --- a/lib/src/user.dart +++ b/lib/src/user.dart @@ -110,13 +110,13 @@ class User extends _BaseUser { 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, + 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, + custom: (other.custom != null ? Map.of(other.custom!) : null), welcomeMessage: other.welcomeMessage, ); @@ -252,9 +252,9 @@ class User extends _BaseUser { 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, + 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'], diff --git a/test/talkjs_test.dart b/test/talkjs_test.dart index e407744..fec99da 100644 --- a/test/talkjs_test.dart +++ b/test/talkjs_test.dart @@ -7,4 +7,203 @@ void main() { 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); + }); } + From 8ed1b4fe9249626ac01500e39001fdeb494f7392 Mon Sep 17 00:00:00 2001 From: Franco Bugnano Date: Mon, 31 Jan 2022 09:22:56 +0100 Subject: [PATCH 44/52] Removed useless toString override for CustomFieldPredicate --- lib/src/predicate.dart | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/src/predicate.dart b/lib/src/predicate.dart index 225ffe6..1b935d4 100644 --- a/lib/src/predicate.dart +++ b/lib/src/predicate.dart @@ -82,17 +82,6 @@ class CustomFieldPredicate extends FieldPredicate { CustomFieldPredicate.of(CustomFieldPredicate other) : _exists = other._exists, super.of(other); - @override - String toString() { - if (_exists == null) { - return super.toString(); - } else if (_exists!) { - return 'exists'; - } else { - return '!exists'; - } - } - @override dynamic toJson() { if (_exists == null) { From 0b878e9c843b8312f09eda7d39c337be47761178 Mon Sep 17 00:00:00 2001 From: Franco Bugnano Date: Mon, 31 Jan 2022 14:46:42 +0100 Subject: [PATCH 45/52] Added tests for JSON conversion of predicates --- test/talkjs_test.dart | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/talkjs_test.dart b/test/talkjs_test.dart index fec99da..b7faab8 100644 --- a/test/talkjs_test.dart +++ b/test/talkjs_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter_test/flutter_test.dart'; import 'package:talkjs/talkjs.dart'; @@ -205,5 +207,45 @@ void main() { ) , 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"]}' + ); + }); } From 7c7731f46db5010a9b86d01d69c53cd3c9070a4c Mon Sep 17 00:00:00 2001 From: Franco Bugnano Date: Wed, 2 Feb 2022 08:27:59 +0100 Subject: [PATCH 46/52] Create main.yml --- .github/workflows/main.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..30a4bc1 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,36 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + +# 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: + # This workflow contains a single job called "build" + build: + # 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 + - uses: actions/checkout@v2 + + # Runs a single command using the runners shell + - name: Run a one-line script + run: echo Hello, world! + + # Runs a set of commands using the runners shell + - name: Run a multi-line script + run: | + echo Add other actions to build, + echo test, and deploy your project. From 359e33b0598b462a0ebb980fb2f13afb36fd5975 Mon Sep 17 00:00:00 2001 From: Franco Bugnano Date: Wed, 2 Feb 2022 09:14:20 +0100 Subject: [PATCH 47/52] Implemented manual Publish to Pub.dev GitHub Workflow --- .github/workflows/main.yml | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 30a4bc1..58b65f9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,36 +1,33 @@ # This is a basic workflow to help you get started with Actions -name: CI +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 ] + ## 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: - # This workflow contains a single job called "build" - build: + 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 - - uses: actions/checkout@v2 + - name: 'Checkout' + uses: actions/checkout@v2 # required! - # Runs a single command using the runners shell - - name: Run a one-line script - run: echo Hello, world! + - name: '>> Dart package <<' + uses: k-paxian/dart-package-publisher@master + with: + accessToken: ${{ secrets.OAUTH_ACCESS_TOKEN }} + refreshToken: ${{ secrets.OAUTH_REFRESH_TOKEN }} - # Runs a set of commands using the runners shell - - name: Run a multi-line script - run: | - echo Add other actions to build, - echo test, and deploy your project. From ebee11115c3655d0715c15a6143ada32b501bdf4 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 2 Feb 2022 16:26:11 +0300 Subject: [PATCH 48/52] Update pubspec.yaml --- pubspec.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 83f51cc..a22b19b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: talkjs -description: SDK for TalkJS -version: 0.0.1 +description: Official TalkJS SDK for Flutter +version: 0.1.0 homepage: https://talkjs.com environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.17.0" + sdk: ">=2.15.0 <3.0.0" + flutter: ">=2.8.1" dependencies: flutter: From baeb0f77d097d18ed38c00773d51ea2c47347e20 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 2 Feb 2022 16:26:45 +0300 Subject: [PATCH 49/52] Update pubspec.lock This includes an upgrade of the package's dependencies --- pubspec.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index fe45fb3..0adf604 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -94,7 +94,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.2" sky_engine: dependency: transitive description: flutter @@ -176,7 +176,7 @@ packages: name: webview_flutter_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" webview_flutter_wkwebview: dependency: transitive description: @@ -185,5 +185,5 @@ packages: source: hosted version: "2.7.1" sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + dart: ">=2.15.0 <3.0.0" + flutter: ">=2.8.1" From da4c7894d1846fb7718b0bbbd0269de5b5227ddf Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 2 Feb 2022 16:28:38 +0300 Subject: [PATCH 50/52] Remove the extension on TextDirection Dart 2.15 added support for accessing the String value of an enum which makes the extension unnecessary. --- lib/src/chatoptions.dart | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/lib/src/chatoptions.dart b/lib/src/chatoptions.dart index cf6a7b8..7a0c9ea 100644 --- a/lib/src/chatoptions.dart +++ b/lib/src/chatoptions.dart @@ -11,17 +11,6 @@ enum TextDirection { ltr, } -extension TextDirectionString on TextDirection { - /// Converts this enum's values to String. - String getValue() { - switch (this) { - case TextDirection.rtl: - return 'rtl'; - case TextDirection.ltr: - return 'ltr'; - } - } -} /// Settings that affect the behavior of the message field class MessageFieldOptions { @@ -199,7 +188,7 @@ class ChatBoxOptions { final result = {}; if (dir != null) { - result['dir'] = dir!.getValue(); + result['dir'] = dir!.name; } if (messageField != null) { From fc2ef52ee2b5ebafc372ca94715dabc0a8045e10 Mon Sep 17 00:00:00 2001 From: Victor Omondi Date: Wed, 2 Feb 2022 16:29:02 +0300 Subject: [PATCH 51/52] (Chatbox): Simplify _createSession --- lib/src/chatbox.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/src/chatbox.dart b/lib/src/chatbox.dart index c5d2fdb..5a256c4 100644 --- a/lib/src/chatbox.dart +++ b/lib/src/chatbox.dart @@ -174,12 +174,9 @@ class ChatBoxState extends State { options["signature"] = widget.session.signature; } - execute('const options = ${json.encode(options)};'); + options["me"] = getUserVariableName(widget.session.me); - final variableName = getUserVariableName(widget.session.me); - execute('options["me"] = $variableName;'); - - execute('const session = new Talk.Session(options);'); + execute('const session = new Talk.Session(${json.encode(options)});'); } void _createChatBox() { From 748ff2decf5cba562f48584ac0db10f9c4fbcff0 Mon Sep 17 00:00:00 2001 From: Franco Bugnano Date: Wed, 2 Feb 2022 15:06:37 +0100 Subject: [PATCH 52/52] Ready to launch --- CHANGELOG.md | 5 +- LICENSE | 29 ++++++++- README.md | 115 +++++++++++++--------------------- lib/src/chatbox.dart | 9 ++- lib/src/conversationlist.dart | 2 +- pubspec.yaml | 4 +- 6 files changed, 83 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac07159..8ae19cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ -## [0.0.1] - TODO: Add release date. +## 0.1.0 + +- Initial version. -* TODO: Describe initial release. diff --git a/LICENSE b/LICENSE index ba75c69..e67741e 100644 --- a/LICENSE +++ b/LICENSE @@ -1 +1,28 @@ -TODO: Add your license here. +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 b70123d..85c09e8 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,64 @@ -# TalkJS +# TalkJS Flutter SDK -Flutter SDK for [TalkJS](https://talkjs.com) +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). + +![Screenshots of TalkJS running on various devices](https://talkjs.com/images/devices_home.jpg) + +Don't hesitate to +[let us know](https://talkjs.com/?chat) +if you have any questions about TalkJS. ## Requirements -- Dart sdk: ">=2.12.0 <3.0.0" -- Flutter: ">=1.17.0" +- Dart sdk: ">=2.15.0 <3.0.0" +- Flutter: ">=2.8.1" - Android: `minSDKVersion 19` ## Installation -Assumption: You have an existing Flutter Project. You can follow this [guide](https://flutter.dev/docs/get-started/test-drive#create-app) -on how to create a new Flutter Project. - -First, clone this repository on your computer. -```sh -git clone https://github.com/talkjs/flutter-sdk-victor.git -``` - -To add the package as a dependency, edit the dependencies section of -your project's **pubspec.yaml** file in your Flutter project as follows: +Edit the dependencies section of your project's `pubspec.yaml` file in your +Flutter project as follows: ```yaml dependencies: - talkjs: - path: {path to directory containing this repository}/flutter-sdk-victor/talkjs + talkjs_flutter: ^0.1.0 ``` -The path specified should be an absolute path from the root directory of -your system. - -Run the command: ```flutter pub get``` on the command line or through -Android Studio's **Get dependencies** button. - -To debug on Android, put this line in the `android/app/src/main/AndroidManifest.xml` as a property of the ` { options["signature"] = widget.session.signature; } - options["me"] = getUserVariableName(widget.session.me); + execute('const options = ${json.encode(options)};'); - execute('const session = new Talk.Session(${json.encode(options)});'); + final variableName = getUserVariableName(widget.session.me); + execute('options["me"] = $variableName;'); + + execute('const session = new Talk.Session(options);'); } void _createChatBox() { @@ -285,7 +288,7 @@ class ChatBoxState extends State { print('📗 chatbox._webViewCreatedCallback'); } - String htmlData = await rootBundle.loadString('packages/talkjs/assets/index.html'); + 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()); diff --git a/lib/src/conversationlist.dart b/lib/src/conversationlist.dart index 94fccc9..dec4714 100644 --- a/lib/src/conversationlist.dart +++ b/lib/src/conversationlist.dart @@ -200,7 +200,7 @@ class ConversationListState extends State { print('📗 conversationlist._webViewCreatedCallback'); } - String htmlData = await rootBundle.loadString('packages/talkjs/assets/index.html'); + 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()); diff --git a/pubspec.yaml b/pubspec.yaml index a22b19b..ea2d399 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,4 +1,4 @@ -name: talkjs +name: talkjs_flutter description: Official TalkJS SDK for Flutter version: 0.1.0 homepage: https://talkjs.com @@ -24,7 +24,7 @@ dev_dependencies: # The following section is specific to Flutter. flutter: assets: - - packages/talkjs/assets/index.html + - packages/talkjs_flutter/assets/index.html # To add assets to your package, add an assets section, like this: # assets: