From de483aa9728f5c8bb7cbef6a2ae612619b9fb754 Mon Sep 17 00:00:00 2001 From: Franco Bugnano Date: Thu, 10 Mar 2022 15:41:58 +0100 Subject: [PATCH 1/2] Updated JS event handlers and implemented onCustomMessageAction --- lib/src/chatbox.dart | 67 +++++++++++++++++++++++++- lib/src/conversationlist.dart | 2 +- lib/src/message.dart | 91 +++++++++++++++++++++++++++++++++++ pubspec.lock | 9 +++- 4 files changed, 165 insertions(+), 4 deletions(-) diff --git a/lib/src/chatbox.dart b/lib/src/chatbox.dart index 407e67c..c35a769 100644 --- a/lib/src/chatbox.dart +++ b/lib/src/chatbox.dart @@ -17,6 +17,7 @@ import './predicate.dart'; typedef SendMessageHandler = void Function(SendMessageEvent event); typedef TranslationToggledHandler = void Function(TranslationToggledEvent event); typedef LoadingStateHandler = void Function(LoadingState state); +typedef MessageActionHandler = void Function(MessageActionEvent event); class SendMessageEvent { final ConversationData conversation; @@ -40,6 +41,15 @@ class TranslationToggledEvent { enum LoadingState { loading, loaded } +class MessageActionEvent { + final String action; + final Message message; + + MessageActionEvent.fromJson(Map json) + : action = json['action'], + message = Message.fromJson(json['message']); +} + /// A messaging UI for just a single conversation. /// /// Create a Chatbox through [Session.createChatbox] and then call [mount] to show it. @@ -62,6 +72,7 @@ class ChatBox extends StatefulWidget { final SendMessageHandler? onSendMessage; final TranslationToggledHandler? onTranslationToggled; final LoadingStateHandler? onLoadingStateChanged; + final Map? onCustomMessageAction; const ChatBox({ Key? key, @@ -79,6 +90,7 @@ class ChatBox extends StatefulWidget { this.onSendMessage, this.onTranslationToggled, this.onLoadingStateChanged, + this.onCustomMessageAction, }) : super(key: key); @override @@ -116,6 +128,7 @@ class ChatBoxState extends State { MessagePredicate _oldMessageFilter = const MessagePredicate(); bool? _oldAsGuest; Conversation? _oldConversation; + Set _oldCustomActions = {}; @override Widget build(BuildContext context) { @@ -151,6 +164,7 @@ class ChatBoxState extends State { // messageFilter and highlightedWords are set as options for the chatbox _createConversation(); } else { + _checkActionHandlers(); _checkMessageFilter(); _checkHighlightedWords(); _checkRecreateConversation(); @@ -172,6 +186,7 @@ class ChatBoxState extends State { JavascriptChannel(name: 'JSCSendMessage', onMessageReceived: _jscSendMessage), JavascriptChannel(name: 'JSCTranslationToggled', onMessageReceived: _jscTranslationToggled), JavascriptChannel(name: 'JSCLoadingState', onMessageReceived: _jscLoadingState), + JavascriptChannel(name: 'JSCCustomMessageAction', onMessageReceived: _jscCustomMessageAction), }); } @@ -208,8 +223,17 @@ class ChatBoxState extends State { execute('chatBox = session.createChatbox(${_oldOptions!.getJsonString(this)});'); - execute('chatBox.on("sendMessage", (event) => JSCSendMessage.postMessage(JSON.stringify(event)));'); - execute('chatBox.on("translationToggled", (event) => JSCTranslationToggled.postMessage(JSON.stringify(event)));'); + execute('chatBox.onSendMessage((event) => JSCSendMessage.postMessage(JSON.stringify(event)));'); + execute('chatBox.onTranslationToggled((event) => JSCTranslationToggled.postMessage(JSON.stringify(event)));'); + + if (widget.onCustomMessageAction != null) { + _oldCustomActions = Set.of(widget.onCustomMessageAction!.keys); + for (var action in _oldCustomActions) { + execute('chatBox.onCustomMessageAction("$action", (event) => JSCCustomMessageAction.postMessage(JSON.stringify(["$action", event])));'); + } + } else { + _oldCustomActions = {}; + } } bool _checkRecreateChatBox() { @@ -232,6 +256,34 @@ class ChatBoxState extends State { } } + bool _checkActionHandlers() { + // If there are no handlers specified, then we don't need to create new handlers + if (widget.onCustomMessageAction == null) { + return false; + } + + var customActions = Set.of(widget.onCustomMessageAction!.keys); + + if (!setEquals(customActions, _oldCustomActions)) { + var retval = false; + + // Register only the new event handlers + for (var action in customActions) { + if (!_oldCustomActions.contains(action)) { + _oldCustomActions.add(action); + + execute('chatBox.onCustomMessageAction("$action", (event) => JSCCustomMessageAction.postMessage(JSON.stringify(["$action", event])));'); + + retval = true; + } + } + + return retval; + } else { + return false; + } + } + void _createConversation() { final result = {}; @@ -358,6 +410,17 @@ class ChatBoxState extends State { widget.onLoadingStateChanged?.call(LoadingState.loaded); } + void _jscCustomMessageAction(JavascriptMessage message) { + if (kDebugMode) { + print('📗 chatbox._jscCustomMessageAction: ${message.message}'); + } + + List jsonMessage = json.decode(message.message); + String action = jsonMessage[0]; + + widget.onCustomMessageAction?[action]?.call(MessageActionEvent.fromJson(jsonMessage[1])); + } + /// For internal use only. Implementation detail that may change anytime. /// /// Return a string with a unique ID diff --git a/lib/src/conversationlist.dart b/lib/src/conversationlist.dart index da46619..5c0a717 100644 --- a/lib/src/conversationlist.dart +++ b/lib/src/conversationlist.dart @@ -182,7 +182,7 @@ class ConversationListState extends State { execute('const conversationList = session.createInbox(${options.getJsonString(this)});'); - execute('''conversationList.on("selectConversation", (event) => { + execute('''conversationList.onSelectConversation((event) => { event.preventDefault(); JSCSelectConversation.postMessage(JSON.stringify(event)); }); '''); diff --git a/lib/src/message.dart b/lib/src/message.dart index 57df158..a66dfee 100644 --- a/lib/src/message.dart +++ b/lib/src/message.dart @@ -1,6 +1,11 @@ +import './conversation.dart'; +import './predicate.dart'; +import './user.dart'; enum MessageType { UserMessage, SystemMessage } +enum ContentType { media, text, location } + class Attachment { final String url; final int size; @@ -49,3 +54,89 @@ class SentMessage { location = (json['location'] != null ? List.from(json['location']) : null); } +class Message { + /// Only given if the message contains a file. An object with the URL and filesize (in bytes) of the given file. + final Attachment? attachment; + + /// The message's content + final String body; + + /// The ConversationData that the message belongs to + final ConversationData conversation; + + /// Custom metadata for this conversation + final Map? custom; + + /// The message ID of the message that was sent + final String id; + + /// 'true' if the message was sent by the current user + final bool isByMe; + + /// Only given if the message contains a location. An array of two numbers which represent the longitude and latitude of this location, respectively. Only given if this message is a shared location. + /// + /// Example: + /// [51.481083, -3.178306] + final List? location; + + // Determines how this message was sent + final MessageOrigin origin; + + /// 'true' if the message has been read, 'false' has not been seen yet + final bool read; + + /// The User that sent the message + final UserData? sender; + + /// Contains the user ID for the person that sent the message + final String? senderId; + + /// UNIX timestamp specifying when the message was sent (UTC, in milliseconds) + final double timestamp; + + /// Specifies if if the message is media (file), text or a shared location + final ContentType type; + + Message.fromJson(Map json) + : attachment = (json['attachment'] != null ? Attachment.fromJson(json['attachment']) : null), + body = json['body'], + conversation = ConversationData.fromJson(json['conversation']), + custom = (json['custom'] != null ? Map.from(json['custom']) : null), + id = json['id'], + isByMe = json['isByMe'], + location = (json['location'] != null ? List.from(json['location']) : null), + origin = _originFromString(json['origin']), + read = json['read'], + sender = (json['sender'] != null ? UserData.fromJson(json['sender']) : null), + senderId = json['senderId'], + timestamp = json['timestamp'].toDouble(), + type = _contentTypeFromString(json['type']); +} + +MessageOrigin _originFromString(String str) { + switch (str) { + case 'web': + return MessageOrigin.web; + case 'rest': + return MessageOrigin.rest; + case 'email': + return MessageOrigin.email; + case 'import': + return MessageOrigin.import; + default: + throw ArgumentError('Unknown MessageOrigin $str'); + } +} + +ContentType _contentTypeFromString(String str) { + switch (str) { + case 'media': + return ContentType.media; + case 'text': + return ContentType.text; + case 'location': + return ContentType.location; + default: + throw ArgumentError('Unknown ContentType $str'); + } +} diff --git a/pubspec.lock b/pubspec.lock index 0adf604..b6cbd8a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -74,6 +74,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" meta: dependency: transitive description: @@ -141,7 +148,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.3" + version: "0.4.8" typed_data: dependency: transitive description: From 271efb8e13cc4e1a34e91dadff26d07865c7358c Mon Sep 17 00:00:00 2001 From: Franco Bugnano Date: Mon, 21 Mar 2022 15:26:36 +0100 Subject: [PATCH 2/2] Simplified custom message action handling --- lib/src/chatbox.dart | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/src/chatbox.dart b/lib/src/chatbox.dart index c35a769..1c3f11e 100644 --- a/lib/src/chatbox.dart +++ b/lib/src/chatbox.dart @@ -145,6 +145,11 @@ class ChatBoxState extends State { Timer.run(() => widget.onLoadingStateChanged?.call(LoadingState.loading)); execute('let chatBox;'); + execute(''' + function customMessageActionHandler(event) { + JSCCustomMessageAction.postMessage(JSON.stringify(event)); + } + '''); _createSession(); _createChatBox(); @@ -229,7 +234,7 @@ class ChatBoxState extends State { if (widget.onCustomMessageAction != null) { _oldCustomActions = Set.of(widget.onCustomMessageAction!.keys); for (var action in _oldCustomActions) { - execute('chatBox.onCustomMessageAction("$action", (event) => JSCCustomMessageAction.postMessage(JSON.stringify(["$action", event])));'); + execute('chatBox.onCustomMessageAction("$action", customMessageActionHandler);'); } } else { _oldCustomActions = {}; @@ -268,11 +273,15 @@ class ChatBoxState extends State { var retval = false; // Register only the new event handlers + // + // Possible memory leak: old event handlers are not getting unregistered + // This should not be a big problem in practice, as it is *very* rare that + // custom message handlers are being constantly changed for (var action in customActions) { if (!_oldCustomActions.contains(action)) { _oldCustomActions.add(action); - execute('chatBox.onCustomMessageAction("$action", (event) => JSCCustomMessageAction.postMessage(JSON.stringify(["$action", event])));'); + execute('chatBox.onCustomMessageAction("$action", customMessageActionHandler);'); retval = true; } @@ -415,10 +424,10 @@ class ChatBoxState extends State { print('📗 chatbox._jscCustomMessageAction: ${message.message}'); } - List jsonMessage = json.decode(message.message); - String action = jsonMessage[0]; + Map jsonMessage = json.decode(message.message); + String action = jsonMessage['action']; - widget.onCustomMessageAction?[action]?.call(MessageActionEvent.fromJson(jsonMessage[1])); + widget.onCustomMessageAction?[action]?.call(MessageActionEvent.fromJson(jsonMessage)); } /// For internal use only. Implementation detail that may change anytime.