Line data Source code
1 : import 'package:chatwoot_client_sdk/chatwoot_callbacks.dart';
2 : import 'package:chatwoot_client_sdk/chatwoot_client.dart';
3 : import 'package:chatwoot_client_sdk/data/local/entity/chatwoot_message.dart';
4 : import 'package:chatwoot_client_sdk/data/local/entity/chatwoot_user.dart';
5 : import 'package:chatwoot_client_sdk/data/remote/chatwoot_client_exception.dart';
6 : import 'package:chatwoot_client_sdk/ui/chatwoot_chat_theme.dart';
7 : import 'package:flutter/material.dart';
8 : import 'package:flutter/widgets.dart';
9 : import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
10 : import 'package:flutter_chat_ui/flutter_chat_ui.dart';
11 : import 'package:intl/intl.dart';
12 : import 'package:uuid/uuid.dart';
13 :
14 : ///Chatwoot chat widget
15 : /// {@category FlutterClientSdk}
16 : class ChatwootChat extends StatefulWidget {
17 : /// Specifies a custom app bar for chatwoot page widget
18 : final PreferredSizeWidget? appBar;
19 :
20 : ///Installation url for chatwoot
21 : final String baseUrl;
22 :
23 : ///Identifier for target chatwoot inbox.
24 : ///
25 : /// For more details see https://www.chatwoot.com/docs/product/channels/api/client-apis
26 : final String inboxIdentifier;
27 :
28 : /// Enables persistence of chatwoot client instance's contact, conversation and messages to disk
29 : /// for convenience.
30 : ///
31 : /// Setting [enablePersistence] to false holds chatwoot client instance's data in memory and is cleared as
32 : /// soon as chatwoot client instance is disposed
33 : final bool enablePersistence;
34 :
35 : /// Custom user details to be attached to chatwoot contact
36 : final ChatwootUser? user;
37 :
38 : /// See [ChatList.onEndReached]
39 : final Future<void> Function()? onEndReached;
40 :
41 : /// See [ChatList.onEndReachedThreshold]
42 : final double? onEndReachedThreshold;
43 :
44 : /// See [Message.onMessageLongPress]
45 : final void Function(types.Message)? onMessageLongPress;
46 :
47 : /// See [Message.onMessageTap]
48 : final void Function(types.Message)? onMessageTap;
49 :
50 : /// See [Input.onSendPressed]
51 : final void Function(types.PartialText)? onSendPressed;
52 :
53 : /// See [Input.onTextChanged]
54 : final void Function(String)? onTextChanged;
55 :
56 : /// See [Message.showUserAvatars]
57 : final bool showUserAvatars;
58 :
59 : /// Show user names for received messages. Useful for a group chat. Will be
60 : /// shown only on text messages.
61 : final bool showUserNames;
62 :
63 : final ChatwootChatTheme? theme;
64 :
65 : /// See [ChatL10n]
66 : final ChatL10n? l10n;
67 :
68 : /// See [Chat.timeFormat]
69 : final DateFormat? timeFormat;
70 :
71 : /// See [Chat.dateFormat]
72 : final DateFormat? dateFormat;
73 :
74 : ///See [ChatwootCallbacks.onWelcome]
75 : final void Function()? onWelcome;
76 :
77 : ///See [ChatwootCallbacks.onPing]
78 : final void Function()? onPing;
79 :
80 : ///See [ChatwootCallbacks.onConfirmedSubscription]
81 : final void Function()? onConfirmedSubscription;
82 :
83 : ///See [ChatwootCallbacks.onConversationStartedTyping]
84 : final void Function()? onConversationStartedTyping;
85 :
86 : ///See [ChatwootCallbacks.onConversationIsOnline]
87 : final void Function()? onConversationIsOnline;
88 :
89 : ///See [ChatwootCallbacks.onConversationIsOffline]
90 : final void Function()? onConversationIsOffline;
91 :
92 : ///See [ChatwootCallbacks.onConversationStoppedTyping]
93 : final void Function()? onConversationStoppedTyping;
94 :
95 : ///See [ChatwootCallbacks.onMessageReceived]
96 : final void Function(ChatwootMessage)? onMessageReceived;
97 :
98 : ///See [ChatwootCallbacks.onMessageSent]
99 : final void Function(ChatwootMessage)? onMessageSent;
100 :
101 : ///See [ChatwootCallbacks.onMessageDelivered]
102 : final void Function(ChatwootMessage)? onMessageDelivered;
103 :
104 : ///See [ChatwootCallbacks.onPersistedMessagesRetrieved]
105 : final void Function(List<ChatwootMessage>)? onPersistedMessagesRetrieved;
106 :
107 : ///See [ChatwootCallbacks.onMessagesRetrieved]
108 : final void Function(List<ChatwootMessage>)? onMessagesRetrieved;
109 :
110 : ///See [ChatwootCallbacks.onError]
111 : final void Function(ChatwootClientException)? onError;
112 :
113 0 : const ChatwootChat({
114 : Key? key,
115 : required this.baseUrl,
116 : required this.inboxIdentifier,
117 : this.enablePersistence = true,
118 : this.user,
119 : this.appBar,
120 : this.onEndReached,
121 : this.onEndReachedThreshold,
122 : this.onMessageLongPress,
123 : this.onMessageTap,
124 : this.onSendPressed,
125 : this.onTextChanged,
126 : this.showUserAvatars = true,
127 : this.showUserNames = true,
128 : this.theme,
129 : this.l10n,
130 : this.timeFormat,
131 : this.dateFormat,
132 : this.onWelcome,
133 : this.onPing,
134 : this.onConfirmedSubscription,
135 : this.onMessageReceived,
136 : this.onMessageSent,
137 : this.onMessageDelivered,
138 : this.onPersistedMessagesRetrieved,
139 : this.onMessagesRetrieved,
140 : this.onConversationStartedTyping,
141 : this.onConversationStoppedTyping,
142 : this.onConversationIsOnline,
143 : this.onConversationIsOffline,
144 : this.onError,
145 0 : }) : super(key: key);
146 :
147 0 : @override
148 0 : _ChatwootChatState createState() => _ChatwootChatState();
149 : }
150 :
151 : class _ChatwootChatState extends State<ChatwootChat> {
152 : ///
153 : List<types.Message> _messages = [];
154 :
155 : late String status;
156 :
157 : final idGen = Uuid();
158 : late final _user;
159 : late final ChatwootClient chatwootClient;
160 :
161 : late final chatwootCallbacks;
162 :
163 0 : @override
164 : void initState() {
165 0 : super.initState();
166 :
167 0 : if (widget.user == null) {
168 0 : _user = types.User(id: idGen.v4());
169 : } else {
170 0 : _user = types.User(
171 0 : id: widget.user?.identifier ?? idGen.v4(),
172 0 : firstName: widget.user?.name,
173 0 : imageUrl: widget.user?.avatarUrl,
174 : );
175 : }
176 :
177 0 : chatwootCallbacks = ChatwootCallbacks(
178 0 : onWelcome: () {
179 0 : widget.onWelcome?.call();
180 : },
181 0 : onPing: () {
182 0 : widget.onPing?.call();
183 : },
184 0 : onConfirmedSubscription: () {
185 0 : widget.onConfirmedSubscription?.call();
186 : },
187 0 : onConversationStartedTyping: () {
188 0 : widget.onConversationStoppedTyping?.call();
189 : },
190 0 : onConversationStoppedTyping: () {
191 0 : widget.onConversationStartedTyping?.call();
192 : },
193 0 : onPersistedMessagesRetrieved: (persistedMessages) {
194 0 : if (widget.enablePersistence) {
195 0 : setState(() {
196 0 : _messages = persistedMessages
197 0 : .map((message) => _chatwootMessageToTextMessage(message))
198 0 : .toList();
199 : });
200 : }
201 0 : widget.onPersistedMessagesRetrieved?.call(persistedMessages);
202 : },
203 0 : onMessagesRetrieved: (messages) {
204 0 : if (messages.isEmpty) {
205 : return;
206 : }
207 0 : setState(() {
208 : final chatMessages = messages
209 0 : .map((message) => _chatwootMessageToTextMessage(message))
210 0 : .toList();
211 : final mergedMessages =
212 0 : <types.Message>[..._messages, ...chatMessages].toSet().toList();
213 0 : final now = DateTime.now().millisecondsSinceEpoch;
214 0 : mergedMessages.sort((a, b) {
215 0 : return (b.createdAt ?? now).compareTo(a.createdAt ?? now);
216 : });
217 0 : _messages = mergedMessages;
218 : });
219 0 : widget.onMessagesRetrieved?.call(messages);
220 : },
221 0 : onMessageReceived: (chatwootMessage) {
222 0 : _addMessage(_chatwootMessageToTextMessage(chatwootMessage));
223 0 : widget.onMessageReceived?.call(chatwootMessage);
224 : },
225 0 : onMessageDelivered: (chatwootMessage, echoId) {
226 0 : _handleMessageSent(
227 0 : _chatwootMessageToTextMessage(chatwootMessage, echoId: echoId));
228 0 : widget.onMessageDelivered?.call(chatwootMessage);
229 : },
230 0 : onMessageSent: (chatwootMessage, echoId) {
231 0 : final textMessage = types.TextMessage(
232 : id: echoId,
233 0 : author: _user,
234 0 : text: chatwootMessage.content ?? "",
235 : status: types.Status.delivered);
236 0 : _handleMessageSent(textMessage);
237 0 : widget.onMessageSent?.call(chatwootMessage);
238 : },
239 0 : onError: (error) {
240 0 : if (error.type == ChatwootClientExceptionType.SEND_MESSAGE_FAILED) {
241 0 : _handleSendMessageFailed(error.data);
242 : }
243 0 : print("Ooops! Something went wrong. Error Cause: ${error.cause}");
244 0 : widget.onError?.call(error);
245 : },
246 : );
247 :
248 0 : ChatwootClient.create(
249 0 : baseUrl: widget.baseUrl,
250 0 : inboxIdentifier: widget.inboxIdentifier,
251 0 : user: widget.user,
252 0 : enablePersistence: widget.enablePersistence,
253 0 : callbacks: chatwootCallbacks)
254 0 : .then((client) {
255 0 : setState(() {
256 0 : chatwootClient = client;
257 0 : chatwootClient.loadMessages();
258 : });
259 0 : }).onError((error, stackTrace) {
260 0 : widget.onError?.call(ChatwootClientException(
261 0 : error.toString(), ChatwootClientExceptionType.CREATE_CLIENT_FAILED));
262 0 : print("chatwoot client failed with error $error: $stackTrace");
263 : });
264 : }
265 :
266 0 : types.TextMessage _chatwootMessageToTextMessage(ChatwootMessage message,
267 : {String? echoId}) {
268 0 : String? avatarUrl = message.sender?.avatarUrl ?? message.sender?.thumbnail;
269 :
270 : //Sets avatar url to null if its a gravatar not found url
271 : //This enables placeholder for avatar to show
272 0 : if (avatarUrl?.contains("?d=404") ?? false) {
273 : avatarUrl = null;
274 : }
275 0 : return types.TextMessage(
276 0 : id: echoId ?? message.id.toString(),
277 0 : author: message.isMine
278 0 : ? _user
279 0 : : types.User(
280 0 : id: message.sender?.id.toString() ?? idGen.v4(),
281 0 : firstName: message.sender?.name,
282 : imageUrl: avatarUrl,
283 : ),
284 0 : text: message.content ?? "",
285 : status: types.Status.seen,
286 0 : createdAt: DateTime.parse(message.createdAt).millisecondsSinceEpoch);
287 : }
288 :
289 0 : void _addMessage(types.Message message) {
290 0 : setState(() {
291 0 : _messages.insert(0, message);
292 : });
293 : }
294 :
295 0 : void _handleSendMessageFailed(String echoId) async {
296 0 : final index = _messages.indexWhere((element) => element.id == echoId);
297 0 : setState(() {
298 0 : _messages[index] = _messages[index].copyWith(status: types.Status.error);
299 : });
300 : }
301 :
302 0 : void _handleResendMessage(types.TextMessage message) async {
303 0 : chatwootClient.sendMessage(content: message.text, echoId: message.id);
304 0 : final index = _messages.indexWhere((element) => element.id == message.id);
305 0 : setState(() {
306 0 : _messages[index] = message.copyWith(status: types.Status.sending);
307 : });
308 : }
309 :
310 0 : void _handleMessageTap(types.Message message) async {
311 0 : if (message.status == types.Status.error && message is types.TextMessage) {
312 0 : _handleResendMessage(message);
313 : }
314 0 : widget.onMessageTap?.call(message);
315 : }
316 :
317 0 : void _handlePreviewDataFetched(
318 : types.TextMessage message,
319 : types.PreviewData previewData,
320 : ) {
321 0 : final index = _messages.indexWhere((element) => element.id == message.id);
322 0 : final updatedMessage = _messages[index].copyWith(previewData: previewData);
323 :
324 0 : WidgetsBinding.instance?.addPostFrameCallback((_) {
325 0 : setState(() {
326 0 : _messages[index] = updatedMessage;
327 : });
328 : });
329 : }
330 :
331 0 : void _handleMessageSent(
332 : types.Message message,
333 : ) {
334 0 : final index = _messages.indexWhere((element) => element.id == message.id);
335 :
336 0 : if (_messages[index].status == types.Status.seen) {
337 : return;
338 : }
339 :
340 0 : WidgetsBinding.instance?.addPostFrameCallback((_) {
341 0 : setState(() {
342 0 : _messages[index] = message;
343 : });
344 : });
345 : }
346 :
347 0 : void _handleSendPressed(types.PartialText message) {
348 0 : final textMessage = types.TextMessage(
349 0 : author: _user,
350 0 : createdAt: DateTime.now().millisecondsSinceEpoch,
351 0 : id: const Uuid().v4(),
352 0 : text: message.text,
353 : status: types.Status.sending);
354 :
355 0 : _addMessage(textMessage);
356 :
357 0 : chatwootClient.sendMessage(
358 0 : content: textMessage.text, echoId: textMessage.id);
359 0 : widget.onSendPressed?.call(message);
360 : }
361 :
362 0 : @override
363 : Widget build(BuildContext context) {
364 0 : return Scaffold(
365 0 : appBar: widget.appBar,
366 0 : backgroundColor: widget.theme?.backgroundColor ?? CHATWOOT_BG_COLOR,
367 0 : body: Padding(
368 : padding: const EdgeInsets.only(bottom: 16.0, left: 16.0, right: 16.0),
369 0 : child: Chat(
370 0 : messages: _messages,
371 0 : onMessageTap: _handleMessageTap,
372 0 : onPreviewDataFetched: _handlePreviewDataFetched,
373 0 : onSendPressed: _handleSendPressed,
374 0 : user: _user,
375 0 : onEndReached: widget.onEndReached,
376 0 : onEndReachedThreshold: widget.onEndReachedThreshold,
377 0 : onMessageLongPress: widget.onMessageLongPress,
378 0 : onTextChanged: widget.onTextChanged,
379 0 : showUserAvatars: widget.showUserAvatars,
380 0 : showUserNames: widget.showUserNames,
381 0 : timeFormat: widget.timeFormat ?? DateFormat.Hm(),
382 0 : dateFormat: widget.timeFormat ?? DateFormat("EEEE MMMM d"),
383 0 : theme: widget.theme ?? ChatwootChatTheme(),
384 0 : l10n: widget.l10n ??
385 0 : ChatL10nEn(
386 : emptyChatPlaceholder: "",
387 : inputPlaceholder: "Type your message"),
388 : ),
389 : ),
390 : );
391 : }
392 :
393 0 : @override
394 : void dispose() {
395 0 : super.dispose();
396 0 : chatwootClient.dispose();
397 : }
398 : }
|