Skip to content

Commit 8492b9d

Browse files
author
Franco Bugnano
committed
Refactor to allow calling build() multiple times on the widgets
1 parent e857775 commit 8492b9d

File tree

7 files changed

+448
-169
lines changed

7 files changed

+448
-169
lines changed

lib/src/chatbox.dart

Lines changed: 161 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ import 'dart:convert';
22

33
import 'package:flutter/material.dart';
44
import 'package:flutter/services.dart';
5-
import 'package:flutter/foundation.dart' show kDebugMode;
5+
import 'package:flutter/foundation.dart';
66

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

99
import 'package:provider/provider.dart';
1010

1111
import './session.dart';
1212
import './conversation.dart';
13-
import './webview.dart';
1413
import './chatoptions.dart';
1514
import './user.dart';
1615
import './message.dart';
@@ -90,6 +89,7 @@ class ChatBox extends StatefulWidget {
9089
class ChatBoxState extends State<ChatBox> {
9190
/// Used to control the underlying WebView
9291
WebViewController? _webViewController;
92+
bool _webViewCreated = false;
9393

9494
/// List of JavaScript statements that haven't been executed.
9595
final _pending = <String>[];
@@ -100,30 +100,72 @@ class ChatBoxState extends State<ChatBox> {
100100
/// A mapping of user ids to the variable name of the respective JavaScript
101101
/// Talk.User object.
102102
final _users = <String, String>{};
103+
final _userObjs = <String, User>{};
103104

104105
/// A mapping of conversation ids to the variable name of the respective JavaScript
105106
/// Talk.ConversationBuilder object.
106107
final _conversations = <String, String>{};
108+
final _conversationObjs = <String, Conversation>{};
107109

108110
/// Encapsulates the message entry field tied to the currently selected conversation.
109111
// TODO: messageField still needs to be refactored
110112
//late MessageField messageField;
111113

114+
/// Objects stored for comparing changes
115+
ChatBoxOptions? _oldOptions;
116+
bool? _oldAsGuest;
117+
Conversation? _oldConversation;
118+
112119
@override
113120
Widget build(BuildContext context) {
121+
if (kDebugMode) {
122+
print('📗 chatbox.build (_webViewCreated: $_webViewCreated)');
123+
}
124+
114125
final sessionState = context.read<SessionState>();
115126

116-
_createSession(sessionState);
117-
_createChatBox();
118-
_createConversation();
127+
if (!_webViewCreated) {
128+
// If it's the first time that the widget is built, then build everything
129+
_webViewCreated = true;
130+
131+
execute('let chatBox;');
132+
133+
_createSession(sessionState);
134+
_createChatBox();
135+
_createConversation();
119136

120-
execute('chatBox.mount(document.getElementById("talkjs-container"));');
137+
execute('chatBox.mount(document.getElementById("talkjs-container"));');
138+
} else {
139+
// If it's not the first time that the widget is built,
140+
// then check what needs to be rebuilt
141+
142+
// TODO: If something has changed in the Session we should do something
143+
144+
final chatBoxRecreated = _checkRecreateChatBox();
145+
146+
if (chatBoxRecreated) {
147+
_createConversation();
148+
} else {
149+
_checkRecreateConversation();
150+
}
121151

122-
return ChatWebView(_webViewCreatedCallback, _onPageFinished, <JavascriptChannel>{
123-
JavascriptChannel(name: 'JSCBlur', onMessageReceived: _jscBlur),
124-
JavascriptChannel(name: 'JSCFocus', onMessageReceived: _jscFocus),
125-
JavascriptChannel(name: 'JSCSendMessage', onMessageReceived: _jscSendMessage),
126-
JavascriptChannel(name: 'JSCTranslationToggled', onMessageReceived: _jscTranslationToggled),
152+
// Mount the chatbox only if it's new (else the existing chatbox has already been mounted)
153+
if (chatBoxRecreated) {
154+
execute('chatBox.mount(document.getElementById("talkjs-container"));');
155+
}
156+
}
157+
158+
return WebView(
159+
initialUrl: 'about:blank',
160+
javascriptMode: JavascriptMode.unrestricted,
161+
debuggingEnabled: kDebugMode,
162+
onWebViewCreated: _webViewCreatedCallback,
163+
onPageFinished: _onPageFinished,
164+
javascriptChannels: <JavascriptChannel>{
165+
JavascriptChannel(name: 'JSCBlur', onMessageReceived: _jscBlur),
166+
JavascriptChannel(name: 'JSCFocus', onMessageReceived: _jscFocus),
167+
JavascriptChannel(name: 'JSCSendMessage', onMessageReceived: _jscSendMessage),
168+
JavascriptChannel(name: 'JSCTranslationToggled', onMessageReceived: _jscTranslationToggled),
127169
});
128170
}
129171

@@ -146,7 +188,7 @@ class ChatBoxState extends State<ChatBox> {
146188
}
147189

148190
void _createChatBox() {
149-
final options = ChatBoxOptions(
191+
_oldOptions = ChatBoxOptions(
150192
chatSubtitleMode: widget.chatSubtitleMode,
151193
chatTitleMode: widget.chatTitleMode,
152194
dir: widget.dir,
@@ -159,30 +201,70 @@ class ChatBoxState extends State<ChatBox> {
159201
conversationIdsToTranslate: widget.conversationIdsToTranslate,
160202
);
161203

162-
execute('const chatBox = session.createChatbox(${options.getJsonString(this)});');
204+
execute('chatBox = session.createChatbox(${_oldOptions!.getJsonString(this)});');
163205

164206
execute('chatBox.on("blur", (event) => JSCBlur.postMessage(JSON.stringify(event)));');
165207
execute('chatBox.on("focus", (event) => JSCFocus.postMessage(JSON.stringify(event)));');
166208
execute('chatBox.on("sendMessage", (event) => JSCSendMessage.postMessage(JSON.stringify(event)));');
167209
execute('chatBox.on("translationToggled", (event) => JSCTranslationToggled.postMessage(JSON.stringify(event)));');
168210
}
169211

212+
bool _checkRecreateChatBox() {
213+
final options = ChatBoxOptions(
214+
chatSubtitleMode: widget.chatSubtitleMode,
215+
chatTitleMode: widget.chatTitleMode,
216+
dir: widget.dir,
217+
messageField: widget.messageField,
218+
showChatHeader: widget.showChatHeader,
219+
showTranslationToggle: widget.showTranslationToggle,
220+
theme: widget.theme,
221+
translateConversations: widget.translateConversations,
222+
conversationsToTranslate: widget.conversationsToTranslate,
223+
conversationIdsToTranslate: widget.conversationIdsToTranslate,
224+
);
225+
226+
if (options != _oldOptions) {
227+
execute('chatBox.destroy();');
228+
_createChatBox();
229+
230+
return true;
231+
} else {
232+
return false;
233+
}
234+
}
235+
170236
void _createConversation() {
171237
final result = <String, dynamic>{};
172238

173-
if (widget.asGuest != null) {
174-
result['asGuest'] = widget.asGuest;
239+
_oldAsGuest = widget.asGuest;
240+
if (_oldAsGuest != null) {
241+
result['asGuest'] = _oldAsGuest;
175242
}
176243

177-
if (widget.conversation != null) {
178-
execute('chatBox.select(${getConversationVariableName(widget.conversation!)}, ${json.encode(result)});');
244+
_oldConversation = widget.conversation;
245+
if (_oldConversation != null) {
246+
execute('chatBox.select(${getConversationVariableName(_oldConversation!)}, ${json.encode(result)});');
179247
} else {
180248
// TODO: null or undefined?
181249
execute('chatBox.select(null, ${json.encode(result)});');
182250
}
183251
}
184252

253+
bool _checkRecreateConversation() {
254+
if ((widget.asGuest != _oldAsGuest) || (widget.conversation != _oldConversation)) {
255+
_createConversation();
256+
257+
return true;
258+
}
259+
260+
return false;
261+
}
262+
185263
void _webViewCreatedCallback(WebViewController webViewController) async {
264+
if (kDebugMode) {
265+
print('📗 chatbox._webViewCreatedCallback');
266+
}
267+
186268
String htmlData = await rootBundle.loadString('packages/talkjs/assets/index.html');
187269
Uri uri = Uri.dataFromString(htmlData, mimeType: 'text/html', encoding: Encoding.getByName('utf-8'));
188270
webViewController.loadUrl(uri.toString());
@@ -191,6 +273,10 @@ class ChatBoxState extends State<ChatBox> {
191273
}
192274

193275
void _onPageFinished(String url) {
276+
if (kDebugMode) {
277+
print('📗 chatbox._onPageFinished');
278+
}
279+
194280
if (url != 'about:blank') {
195281
// Wait for TalkJS to be ready
196282
// Not all WebViews support top level await, so it's better to use an
@@ -266,8 +352,17 @@ class ChatBoxState extends State<ChatBox> {
266352
// Generate unique variable name
267353
final variableName = 'user${getUniqueId()}';
268354

269-
execute('const $variableName = new Talk.User(${user.getJsonString()});');
270355
_users[user.id] = variableName;
356+
357+
execute('let $variableName = new Talk.User(${user.getJsonString()});');
358+
359+
_userObjs[user.id] = User.of(user);
360+
} else if (_userObjs[user.id] != user) {
361+
final variableName = _users[user.id]!;
362+
363+
execute('$variableName = new Talk.User(${user.getJsonString()});');
364+
365+
_userObjs[user.id] = User.of(user);
271366
}
272367

273368
return _users[user.id]!;
@@ -276,54 +371,70 @@ class ChatBoxState extends State<ChatBox> {
276371
/// For internal use only. Implementation detail that may change anytime.
277372
String getConversationVariableName(Conversation conversation) {
278373
if (_conversations[conversation.id] == null) {
279-
// STEP 1: Generate unique variable name
280374
final variableName = 'conversation${getUniqueId()}';
281375

282-
execute('const $variableName = session.getOrCreateConversation("${conversation.id}")');
376+
_conversations[conversation.id] = variableName;
283377

284-
// STEP 2: Attributes
285-
final attributes = <String, dynamic>{};
378+
execute('let $variableName = session.getOrCreateConversation("${conversation.id}")');
286379

287-
if (conversation.custom != null) {
288-
attributes['custom'] = conversation.custom;
289-
}
380+
_setConversationAttributes(variableName, conversation);
381+
_setConversationParticipants(variableName, conversation);
290382

291-
if (conversation.welcomeMessages != null) {
292-
attributes['welcomeMessages'] = conversation.welcomeMessages;
293-
}
383+
_conversationObjs[conversation.id] = Conversation.of(conversation);
384+
} else if (_conversationObjs[conversation.id] != conversation) {
385+
final variableName = _conversations[conversation.id]!;
294386

295-
if (conversation.photoUrl != null) {
296-
attributes['photoUrl'] = conversation.photoUrl;
297-
}
387+
_setConversationAttributes(variableName, conversation);
298388

299-
if (conversation.subject != null) {
300-
attributes['subject'] = conversation.subject;
389+
if (!setEquals(conversation.participants, _conversationObjs[conversation.id]!.participants)) {
390+
_setConversationParticipants(variableName, conversation);
301391
}
302392

303-
if (attributes.isNotEmpty) {
304-
execute('$variableName.setAttributes(${json.encode(attributes)});');
305-
}
393+
_conversationObjs[conversation.id] = Conversation.of(conversation);
394+
}
306395

307-
// STEP 3: Participants
308-
for (var participant in conversation.participants) {
309-
final userVariableName = getUserVariableName(participant.user);
310-
final result = <String, dynamic>{};
396+
return _conversations[conversation.id]!;
397+
}
311398

312-
if (participant.access != null) {
313-
result['access'] = participant.access!.getValue();
314-
}
399+
void _setConversationAttributes(String variableName, Conversation conversation) {
400+
final attributes = <String, dynamic>{};
315401

316-
if (participant.notify != null) {
317-
result['notify'] = participant.notify;
318-
}
402+
if (conversation.custom != null) {
403+
attributes['custom'] = conversation.custom;
404+
}
319405

320-
execute('$variableName.setParticipant($userVariableName, ${json.encode(result)});');
321-
}
406+
if (conversation.welcomeMessages != null) {
407+
attributes['welcomeMessages'] = conversation.welcomeMessages;
408+
}
322409

323-
_conversations[conversation.id] = variableName;
410+
if (conversation.photoUrl != null) {
411+
attributes['photoUrl'] = conversation.photoUrl;
324412
}
325413

326-
return _conversations[conversation.id]!;
414+
if (conversation.subject != null) {
415+
attributes['subject'] = conversation.subject;
416+
}
417+
418+
if (attributes.isNotEmpty) {
419+
execute('$variableName.setAttributes(${json.encode(attributes)});');
420+
}
421+
}
422+
423+
void _setConversationParticipants(String variableName, Conversation conversation) {
424+
for (var participant in conversation.participants) {
425+
final userVariableName = getUserVariableName(participant.user);
426+
final result = <String, dynamic>{};
427+
428+
if (participant.access != null) {
429+
result['access'] = participant.access!.getValue();
430+
}
431+
432+
if (participant.notify != null) {
433+
result['notify'] = participant.notify;
434+
}
435+
436+
execute('$variableName.setParticipant($userVariableName, ${json.encode(result)});');
437+
}
327438
}
328439

329440
/// For internal use only. Implementation detail that may change anytime.
@@ -342,37 +453,6 @@ class ChatBoxState extends State<ChatBox> {
342453
this._pending.add(statement);
343454
}
344455
}
345-
346-
/// Destroys this UI element and removes all event listeners it has running.
347-
void destroy() {
348-
execute('chatBox.destroy();');
349-
}
350-
351-
/*
352-
void select(ConversationBuilder? conversation, {bool? asGuest}) {
353-
final result = <String, dynamic>{};
354-
355-
if (asGuest != null) {
356-
result['asGuest'] = asGuest;
357-
}
358-
359-
if (conversation != null) {
360-
execute('chatBox.select(${conversation.variableName}, ${json.encode(result)});');
361-
} else {
362-
execute('chatBox.select(null, ${json.encode(result)});');
363-
}
364-
}
365-
366-
void selectLatestConversation({bool? asGuest}) {
367-
final result = <String, dynamic>{};
368-
369-
if (asGuest != null) {
370-
result['asGuest'] = asGuest;
371-
}
372-
373-
execute('chatBox.select(undefined, ${json.encode(result)});');
374-
}
375-
*/
376456
}
377457

378458
/// Encapsulates the message entry field tied to the currently selected conversation.

0 commit comments

Comments
 (0)