Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 74 additions & 2 deletions lib/src/chatbox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,6 +41,15 @@ class TranslationToggledEvent {

enum LoadingState { loading, loaded }

class MessageActionEvent {
final String action;
final Message message;

MessageActionEvent.fromJson(Map<String, dynamic> 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.
Expand All @@ -62,6 +72,7 @@ class ChatBox extends StatefulWidget {
final SendMessageHandler? onSendMessage;
final TranslationToggledHandler? onTranslationToggled;
final LoadingStateHandler? onLoadingStateChanged;
final Map<String, MessageActionHandler>? onCustomMessageAction;

const ChatBox({
Key? key,
Expand All @@ -79,6 +90,7 @@ class ChatBox extends StatefulWidget {
this.onSendMessage,
this.onTranslationToggled,
this.onLoadingStateChanged,
this.onCustomMessageAction,
}) : super(key: key);

@override
Expand Down Expand Up @@ -116,6 +128,7 @@ class ChatBoxState extends State<ChatBox> {
MessagePredicate _oldMessageFilter = const MessagePredicate();
bool? _oldAsGuest;
Conversation? _oldConversation;
Set<String> _oldCustomActions = {};

@override
Widget build(BuildContext context) {
Expand All @@ -132,6 +145,11 @@ class ChatBoxState extends State<ChatBox> {
Timer.run(() => widget.onLoadingStateChanged?.call(LoadingState.loading));

execute('let chatBox;');
execute('''
function customMessageActionHandler(event) {
JSCCustomMessageAction.postMessage(JSON.stringify(event));
}
''');

_createSession();
_createChatBox();
Expand All @@ -151,6 +169,7 @@ class ChatBoxState extends State<ChatBox> {
// messageFilter and highlightedWords are set as options for the chatbox
_createConversation();
} else {
_checkActionHandlers();
_checkMessageFilter();
_checkHighlightedWords();
_checkRecreateConversation();
Expand All @@ -172,6 +191,7 @@ class ChatBoxState extends State<ChatBox> {
JavascriptChannel(name: 'JSCSendMessage', onMessageReceived: _jscSendMessage),
JavascriptChannel(name: 'JSCTranslationToggled', onMessageReceived: _jscTranslationToggled),
JavascriptChannel(name: 'JSCLoadingState', onMessageReceived: _jscLoadingState),
JavascriptChannel(name: 'JSCCustomMessageAction', onMessageReceived: _jscCustomMessageAction),
});
}

Expand Down Expand Up @@ -208,8 +228,17 @@ class ChatBoxState extends State<ChatBox> {

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<String>.of(widget.onCustomMessageAction!.keys);
for (var action in _oldCustomActions) {
execute('chatBox.onCustomMessageAction("$action", customMessageActionHandler);');
}
} else {
_oldCustomActions = {};
}
}

bool _checkRecreateChatBox() {
Expand All @@ -232,6 +261,38 @@ class ChatBoxState extends State<ChatBox> {
}
}

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<String>.of(widget.onCustomMessageAction!.keys);

if (!setEquals(customActions, _oldCustomActions)) {
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", customMessageActionHandler);');

retval = true;
}
}

return retval;
} else {
return false;
}
}

void _createConversation() {
final result = <String, dynamic>{};

Expand Down Expand Up @@ -358,6 +419,17 @@ class ChatBoxState extends State<ChatBox> {
widget.onLoadingStateChanged?.call(LoadingState.loaded);
}

void _jscCustomMessageAction(JavascriptMessage message) {
if (kDebugMode) {
print('📗 chatbox._jscCustomMessageAction: ${message.message}');
}

Map<String, dynamic> jsonMessage = json.decode(message.message);
String action = jsonMessage['action'];

widget.onCustomMessageAction?[action]?.call(MessageActionEvent.fromJson(jsonMessage));
}

/// For internal use only. Implementation detail that may change anytime.
///
/// Return a string with a unique ID
Expand Down
2 changes: 1 addition & 1 deletion lib/src/conversationlist.dart
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ class ConversationListState extends State<ConversationList> {

execute('const conversationList = session.createInbox(${options.getJsonString(this)});');

execute('''conversationList.on("selectConversation", (event) => {
execute('''conversationList.onSelectConversation((event) => {
event.preventDefault();
JSCSelectConversation.postMessage(JSON.stringify(event));
}); ''');
Expand Down
91 changes: 91 additions & 0 deletions lib/src/message.dart
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -49,3 +54,89 @@ class SentMessage {
location = (json['location'] != null ? List<double>.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<String, String?>? 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<double>? 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<String, dynamic> json)
: attachment = (json['attachment'] != null ? Attachment.fromJson(json['attachment']) : null),
body = json['body'],
conversation = ConversationData.fromJson(json['conversation']),
custom = (json['custom'] != null ? Map<String, String?>.from(json['custom']) : null),
id = json['id'],
isByMe = json['isByMe'],
location = (json['location'] != null ? List<double>.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');
}
}
9 changes: 8 additions & 1 deletion pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down