Skip to content

Commit 99b11eb

Browse files
author
Franco Bugnano
committed
Initial work for ChatBox events
1 parent 39b4e11 commit 99b11eb

File tree

7 files changed

+180
-23
lines changed

7 files changed

+180
-23
lines changed

lib/src/chatbox.dart

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@ import './conversation.dart';
1313
import './webview.dart';
1414
import './chatoptions.dart';
1515
import './user.dart';
16+
import './message.dart';
17+
18+
typedef BlurHandler = void Function();
19+
typedef FocusHandler = void Function();
20+
typedef SendMessageHandler = void Function(SendMessageEvent event);
21+
typedef TranslationToggledHandler = void Function();
22+
23+
class SendMessageEvent {
24+
ConversationData conversation;
25+
User me;
26+
SentMessage message;
27+
28+
SendMessageEvent.fromJson(Map<String, dynamic> json)
29+
: conversation = ConversationData.fromJson(json['conversation']),
30+
me = User.fromJson(json['me']),
31+
message = SentMessage.fromJson(json['message']);
32+
}
1633

1734
/// A messaging UI for just a single conversation.
1835
///
@@ -33,6 +50,11 @@ class ChatBox extends StatefulWidget {
3350
final Conversation? conversation;
3451
final bool? asGuest;
3552

53+
final BlurHandler? onBlur;
54+
final FocusHandler? onFocus;
55+
final SendMessageHandler? onSendMessage;
56+
final TranslationToggledHandler? onTranslationToggled;
57+
3658
const ChatBox({Key? key,
3759
this.chatSubtitleMode,
3860
this.chatTitleMode,
@@ -46,13 +68,17 @@ class ChatBox extends StatefulWidget {
4668
this.conversationIdsToTranslate,
4769
this.conversation,
4870
this.asGuest,
71+
this.onBlur,
72+
this.onFocus,
73+
this.onSendMessage,
74+
this.onTranslationToggled,
4975
}) : super(key: key);
5076

5177
@override
52-
State<ChatBox> createState() => _ChatBoxState();
78+
State<ChatBox> createState() => ChatBoxState();
5379
}
5480

55-
class _ChatBoxState extends State<ChatBox> {
81+
class ChatBoxState extends State<ChatBox> {
5682
/// Used to control the underlying WebView
5783
WebViewController? _webViewController;
5884

@@ -84,7 +110,12 @@ class _ChatBoxState extends State<ChatBox> {
84110

85111
execute('chatBox.mount(document.getElementById("talkjs-container"));');
86112

87-
return ChatWebView(_webViewCreatedCallback, _onPageFinished);
113+
return ChatWebView(_webViewCreatedCallback, _onPageFinished, <JavascriptChannel>{
114+
JavascriptChannel(name: 'JSCBlur', onMessageReceived: _jscBlur),
115+
JavascriptChannel(name: 'JSCFocus', onMessageReceived: _jscFocus),
116+
JavascriptChannel(name: 'JSCSendMessage', onMessageReceived: _jscSendMessage),
117+
JavascriptChannel(name: 'JSCTranslationToggled', onMessageReceived: _jscTranslationToggled),
118+
});
88119
}
89120

90121
void _createSession(SessionState sessionState) {
@@ -119,7 +150,12 @@ class _ChatBoxState extends State<ChatBox> {
119150
conversationIdsToTranslate: widget.conversationIdsToTranslate,
120151
);
121152

122-
execute('const chatBox = session.createChatbox(${options.getJsonString()});');
153+
execute('const chatBox = session.createChatbox(${options.getJsonString(this)});');
154+
155+
execute('chatBox.on("blur", (event) => JSCBlur.postMessage(JSON.stringify(event)));');
156+
execute('chatBox.on("focus", (event) => JSCFocus.postMessage(JSON.stringify(event)));');
157+
execute('chatBox.on("sendMessage", (event) => JSCSendMessage.postMessage(JSON.stringify(event)));');
158+
execute('chatBox.on("translationToggled", (event) => JSCTranslationToggled.postMessage(JSON.stringify(event)));');
123159
}
124160

125161
void _createConversation() {
@@ -156,19 +192,51 @@ class _ChatBoxState extends State<ChatBox> {
156192
print('📗 chatbox._onPageFinished: $js');
157193
}
158194

159-
_webViewController!.runJavascriptReturningResult(js);
195+
_webViewController!.runJavascript(js);
160196

161197
// Execute any pending instructions
162198
for (var statement in _pending) {
163199
if (kDebugMode) {
164200
print('📗 chatbox._onPageFinished _pending: $statement');
165201
}
166202

167-
_webViewController!.runJavascriptReturningResult(statement);
203+
_webViewController!.runJavascript(statement);
168204
}
169205
}
170206
}
171207

208+
void _jscBlur(JavascriptMessage message) {
209+
if (kDebugMode) {
210+
print('📗 chatbox._jscBlur: ${message.message}');
211+
}
212+
213+
widget.onBlur?.call();
214+
}
215+
216+
void _jscFocus(JavascriptMessage message) {
217+
if (kDebugMode) {
218+
print('📗 chatbox._jscFocus: ${message.message}');
219+
}
220+
221+
widget.onFocus?.call();
222+
}
223+
224+
void _jscSendMessage(JavascriptMessage message) {
225+
if (kDebugMode) {
226+
print('📗 chatbox._jscSendMessage: ${message.message}');
227+
}
228+
229+
widget.onSendMessage?.call(SendMessageEvent.fromJson(json.decode(message.message)));
230+
}
231+
232+
void _jscTranslationToggled(JavascriptMessage message) {
233+
if (kDebugMode) {
234+
print('📗 chatbox._jscTranslationToggled: ${message.message}');
235+
}
236+
// {"isEnabled":true,"conversation":{}}
237+
widget.onTranslationToggled?.call();
238+
}
239+
172240
/// For internal use only. Implementation detail that may change anytime.
173241
///
174242
/// Return a string with a unique ID
@@ -259,7 +327,7 @@ class _ChatBoxState extends State<ChatBox> {
259327
}
260328

261329
if (controller != null) {
262-
controller.runJavascriptReturningResult(statement);
330+
controller.runJavascript(statement);
263331
} else {
264332
this._pending.add(statement);
265333
}
@@ -300,7 +368,7 @@ class _ChatBoxState extends State<ChatBox> {
300368
/// Encapsulates the message entry field tied to the currently selected conversation.
301369
class MessageField {
302370
/// The ChatBox associated with this message field
303-
_ChatBoxState chatbox;
371+
ChatBoxState chatbox;
304372

305373
/// The JavaScript variable name for this object.
306374
String variableName;

lib/src/chatoptions.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ abstract class _ChatOptions {
200200
/// The toJson method is intentionally omitted, to produce an error if
201201
/// someone tries to convert this object to JSON instead of using the
202202
/// getJsonString method.
203-
String getJsonString() {
203+
String getJsonString(ChatBoxState chatBox) {
204204
final result = <String, dynamic>{};
205205

206206
if (chatSubtitleMode != null) {
@@ -232,19 +232,17 @@ abstract class _ChatOptions {
232232
result['theme'] = theme;
233233
}
234234

235-
/* TODO: Conversation.variableName is to be done
236235
if (conversationsToTranslate != null) {
237236
// Highest priority: TranslateConversations.off
238237
if (translateConversations != TranslateConversations.off) {
239238
// High priority: conversationsToTranslate
240239
// This results in a string value that will be parsed later
241240
result['translateConversations'] ??= '[' + conversationsToTranslate
242-
!.map((conversation) => conversation.variableName)
241+
!.map((conversation) => chatBox.getConversationVariableName(conversation))
243242
.join(',')
244243
+ ']';
245244
}
246245
}
247-
*/
248246

249247
if (conversationIdsToTranslate != null) {
250248
// Highest priority: TranslateConversations.off

lib/src/conversation.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,31 @@ class Conversation {
122122
*/
123123
}
124124

125+
class ConversationData {
126+
/// The unique conversation identifier.
127+
String id;
128+
129+
/// Custom metadata for this conversation
130+
Map<String, String?>? custom;
131+
132+
/// Messages sent at the beginning of a chat.
133+
///
134+
/// The messages will appear as system messages.
135+
List<String>? welcomeMessages;
136+
137+
/// The URL to a photo which will be shown as the photo for the conversation.
138+
String? photoUrl;
139+
140+
/// The conversation subject which will be displayed in the chat header.
141+
String? subject;
142+
143+
ConversationData.fromJson(Map<String, dynamic> json)
144+
: id = json['id'],
145+
custom = json['custom'] != null ? Map<String, String?>.from(json['custom']) : null,
146+
welcomeMessages = json['welcomeMessages'] != null ? List<String>.from(json['welcomeMessages']) : null,
147+
photoUrl = json['photoUrl'],
148+
subject = json['subject'];
149+
150+
}
151+
152+

lib/src/message.dart

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
2+
enum MessageType { UserMessage, SystemMessage }
3+
4+
class Attachment {
5+
String url;
6+
int size;
7+
8+
Attachment.fromJson(Map<String, dynamic> json)
9+
: url = json['url'],
10+
size = json['size'];
11+
}
12+
13+
class SentMessage {
14+
/// The message ID of the message that was sent
15+
String? id;
16+
17+
/// The ID of the conversation that the message belongs to
18+
String conversationId;
19+
20+
/// Identifies the message as either a User message or System message
21+
MessageType type;
22+
23+
/// Contains an Array of User.id's that have read the message
24+
List<String> readBy;
25+
26+
/// Contains the user ID for the person that sent the message
27+
String senderId; // redundant since the user is always me, but keeps it consistant
28+
29+
/// Contains the message's text
30+
String? text;
31+
32+
/// Only given if the message contains a file. An object with the URL and filesize (in bytes) of the given file.
33+
Attachment? attachment;
34+
35+
/// 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.
36+
///
37+
/// Example:
38+
/// [51.481083, -3.178306]
39+
List<double>? location;
40+
41+
SentMessage.fromJson(Map<String, dynamic> json)
42+
: id = json['id'],
43+
conversationId = json['conversationId'],
44+
type = json['type'] == 'UserMessage' ? MessageType.UserMessage : MessageType.SystemMessage,
45+
readBy = List<String>.from(json['readBy']),
46+
senderId = json['senderId'],
47+
text = json['text'],
48+
attachment = json['attachment'] != null ? Attachment.fromJson(json['attachment']) : null,
49+
location = json['location'] != null ? List<double>.from(json['location']) : null;
50+
}
51+

lib/src/session.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ class Session extends StatelessWidget {
3838
// can own Counter's lifecycle, making sure to call `dispose`
3939
// when not needed anymore.
4040
create: (context) => SessionState(appId: appId, me: me, signature: signature),
41-
child: Container(
42-
child: child,
43-
),
41+
child: child,
4442
);
4543
}
4644
}

lib/src/user.dart

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ class User {
2828
List<String>? phone;
2929

3030
/// The unique user identifier.
31-
String id;
31+
final String id;
3232

3333
/// This user's name which will be displayed on the TalkJS UI
34-
String name;
34+
final String name;
3535

3636
/// The language on the UI.
3737
///
@@ -58,6 +58,19 @@ class User {
5858

5959
User.fromId(this.id) : this.name = '', this._idOnly = true;
6060

61+
User.fromJson(Map<String, dynamic> json)
62+
: availabilityText = json['availabilityText'],
63+
custom = json['custom'] != null ? Map<String, String?>.from(json['custom']) : null,
64+
email = json['email'] != null ? (json['email'] is String ? <String>[json['email']] : List<String>.from(json['email'])) : null,
65+
phone = json['phone'] != null ? (json['phone'] is String ? <String>[json['phone']] : List<String>.from(json['phone'])) : null,
66+
id = json['id'],
67+
name = json['name'],
68+
locale = json['locale'],
69+
photoUrl = json['photoUrl'],
70+
role = json['role'],
71+
welcomeMessage = json['welcomeMessage'],
72+
_idOnly = false;
73+
6174
/// For internal use only. Implementation detail that may change anytime.
6275
///
6376
/// This method is used instead of toJson, as we need to output valid JS
@@ -110,3 +123,4 @@ class User {
110123
}
111124
}
112125
}
126+

lib/src/webview.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import 'dart:io';
22

33
import 'package:flutter/material.dart';
4-
import 'package:flutter/foundation.dart' show kReleaseMode;
4+
import 'package:flutter/foundation.dart' show kDebugMode;
55

66
import 'package:webview_flutter/webview_flutter.dart';
77

88
/// Wrapper around the [WebView] widget.
99
class ChatWebView extends StatefulWidget {
1010
final ChatWebViewState state;
1111

12-
ChatWebView(WebViewCreatedCallback webViewFn, PageFinishedCallback jsFn, {Key? key})
13-
: state = ChatWebViewState(webViewFn, jsFn),
12+
ChatWebView(WebViewCreatedCallback webViewFn, PageFinishedCallback jsFn, Set<JavascriptChannel> javascriptChannels, {Key? key})
13+
: state = ChatWebViewState(webViewFn, jsFn, javascriptChannels),
1414
super(key: key);
1515

1616
@override
@@ -20,8 +20,7 @@ class ChatWebView extends StatefulWidget {
2020
class ChatWebViewState extends State<ChatWebView> {
2121
late WebView webView;
2222

23-
ChatWebViewState(WebViewCreatedCallback webViewFn,
24-
PageFinishedCallback jsFn) {
23+
ChatWebViewState(WebViewCreatedCallback webViewFn, PageFinishedCallback jsFn, Set<JavascriptChannel> javascriptChannels) {
2524
// Enable hybrid composition.
2625
if (Platform.isAndroid) {
2726
WebView.platform = SurfaceAndroidWebView();
@@ -30,9 +29,10 @@ class ChatWebViewState extends State<ChatWebView> {
3029
this.webView = WebView(
3130
initialUrl: 'about:blank',
3231
javascriptMode: JavascriptMode.unrestricted,
33-
debuggingEnabled: !kReleaseMode,
32+
debuggingEnabled: kDebugMode,
3433
onWebViewCreated: webViewFn,
3534
onPageFinished: jsFn,
35+
javascriptChannels: javascriptChannels,
3636
);
3737
}
3838

0 commit comments

Comments
 (0)