Skip to content

Commit 415042a

Browse files
committed
feat: add message translation feature and integrate translator service
1 parent 6c8edbf commit 415042a

File tree

8 files changed

+152
-44
lines changed

8 files changed

+152
-44
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Cross-Platform app for chatwoot! Built with Flutter.
2929
- Reply easily with canned responses
3030
- Receive realtime notifications about system activities
3131
- Communicate with other team members via private notes
32+
- Message translation in conversations
3233
- Assign statuses to your conversations, recorder, execute macro... and more to come!
3334

3435
```
@@ -49,6 +50,7 @@ Cross-Platform app for chatwoot! Built with Flutter.
4950
| UI - Customize theme | ✅ Completed |
5051
| Settings (Partially) | 💪 In-Progress |
5152
| Conversations | 💪 In-Progress |
53+
| Message Translation | ✅ Completed |
5254
| Contacts | 💪 In-Progress |
5355
| Notifications | 💪 In-Progress |
5456
| Push Notifications | ☑️ Not tested |

lib/config/bindings.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class BindingsConfig {
1313
await Get.putAsync(() => AssistantService().init());
1414
await Get.putAsync(() => NotificationService().init());
1515
await Get.putAsync(() => RealtimeService().init());
16+
await Get.putAsync(() => TranslatorService().init());
1617

1718
// global controllers
1819
await Get.putAsync(() => CannedResponsesController().init(),

lib/imports.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export 'services/notification.dart';
7979
export 'services/realtime.dart';
8080
export 'services/settings.dart';
8181
export 'services/theme.dart';
82+
export 'services/translator.dart';
8283
export 'services/updater.dart';
8384

8485
export 'utils/bytes.dart';

lib/screens/conversations/controllers/chat.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'package:chatwoot/screens/conversations/widgets/message_action.dart';
2+
13
import '/screens/contacts/views/detail.dart';
24
import '/imports.dart';
35

@@ -6,13 +8,15 @@ class ConversationChatController extends GetxController {
68
final _api = Get.find<ApiService>();
79
final _auth = Get.find<AuthService>();
810
final _realtime = Get.find<RealtimeService>();
11+
final _translator = Get.find<TranslatorService>();
912

1013
final scrollController = ScrollController();
1114
final int conversation_id;
1215
final loading = false.obs;
1316
final isNoMore = false.obs;
1417
final info = Rxn<ConversationInfo>();
1518
final error = ''.obs;
19+
final translated = RxMap<int, String?>({});
1620
late RxList<MessageInfo> messages;
1721

1822
ConversationChatController({
@@ -194,4 +198,22 @@ class ConversationChatController extends GetxController {
194198
status: status,
195199
);
196200
}
201+
202+
Future<void> showMessageActions(MessageInfo info) async {
203+
await Get.bottomSheet(MessageActions(controller: this, info: info));
204+
}
205+
206+
Future<void> translate(MessageInfo info) async {
207+
translated[info.id] = null;
208+
final result = await _translator.getProvider.translate(
209+
info.content!,
210+
to: Get.locale!,
211+
);
212+
if (result.isError()) {
213+
showSnackBar(result.exceptionOrNull()!.toString());
214+
translated.remove(info.id);
215+
return;
216+
}
217+
translated[info.id] = result.getOrThrow();
218+
}
197219
}

lib/screens/conversations/widgets/message.dart

Lines changed: 58 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1+
import 'package:chatwoot/screens/conversations/controllers/chat.dart';
12
import 'package:flutter_markdown/flutter_markdown.dart';
23
import '/imports.dart';
34

45
class Message extends StatelessWidget {
56
final auth = Get.find<AuthService>();
67
final realtime = Get.find<RealtimeService>();
78

9+
final ConversationChatController controller;
810
final MessageInfo info;
911
final BorderRadius borderRadius;
1012

1113
Message({
1214
super.key,
15+
required this.controller,
1316
required this.info,
1417
required this.borderRadius,
1518
});
@@ -146,54 +149,58 @@ class Message extends StatelessWidget {
146149
}),
147150
),
148151
Flexible(
149-
child: Container(
152+
child: Ink(
150153
decoration: BoxDecoration(
151154
color: backgroundColor,
152155
border: Border.all(color: backgroundColor),
153156
borderRadius: borderRadius,
154157
),
155-
child: Column(
156-
crossAxisAlignment: CrossAxisAlignment.start,
157-
children: [
158-
if (info.attachments.isNotEmpty) buildAttachments(context),
159-
if (info.content != null && info.content!.isNotEmpty)
160-
buildContent(context),
161-
// TODO: align right
162-
Padding(
163-
padding: const EdgeInsets.all(8),
164-
child: Row(
165-
mainAxisSize: MainAxisSize.min,
166-
spacing: 8,
167-
children: [
168-
if (info.private)
169-
Text(
170-
t.private,
171-
style: TextStyle(
172-
fontSize: Get.textTheme.labelSmall!.fontSize,
158+
child: InkWell(
159+
child: Column(
160+
crossAxisAlignment: CrossAxisAlignment.start,
161+
children: [
162+
if (info.attachments.isNotEmpty)
163+
buildAttachments(context),
164+
if (info.content != null && info.content!.isNotEmpty)
165+
buildContent(context),
166+
// TODO: align right
167+
Padding(
168+
padding: const EdgeInsets.all(8),
169+
child: Row(
170+
mainAxisSize: MainAxisSize.min,
171+
spacing: 8,
172+
children: [
173+
if (info.private)
174+
Text(
175+
t.private,
176+
style: TextStyle(
177+
fontSize: Get.textTheme.labelSmall!.fontSize,
178+
),
179+
),
180+
if (isOwner == false)
181+
Text(
182+
sender.display_name,
183+
style: TextStyle(
184+
fontSize: Get.textTheme.labelSmall!.fontSize,
185+
),
173186
),
174-
),
175-
if (isOwner == false)
176187
Text(
177-
sender.display_name,
188+
created_at,
178189
style: TextStyle(
179190
fontSize: Get.textTheme.labelSmall!.fontSize,
180191
),
181192
),
182-
Text(
183-
created_at,
184-
style: TextStyle(
185-
fontSize: Get.textTheme.labelSmall!.fontSize,
186-
),
187-
),
188-
if (statusIcon != null)
189-
Padding(
190-
padding: EdgeInsets.only(right: 4),
191-
child: statusIcon,
192-
),
193-
],
193+
if (statusIcon != null)
194+
Padding(
195+
padding: EdgeInsets.only(right: 4),
196+
child: statusIcon,
197+
),
198+
],
199+
),
194200
),
195-
),
196-
],
201+
],
202+
),
203+
onLongPress: () => controller.showMessageActions(info),
197204
),
198205
),
199206
),
@@ -210,14 +217,21 @@ class Message extends StatelessWidget {
210217
right: 8,
211218
top: 8,
212219
),
213-
child: MarkdownBody(
214-
data: info.content!.trim(),
215-
selectable: true,
216-
onTapLink: (text, href, title) {
217-
if (href == null || href.isEmpty) return;
218-
openInAppBrowser(href);
219-
},
220-
),
220+
child: Obx(() {
221+
final translated = controller.translated.value;
222+
final content = translated.containsKey(info.id) &&
223+
isNotNullOrEmpty(translated[info.id])
224+
? translated[info.id]!
225+
: info.content!.trim();
226+
227+
return MarkdownBody(
228+
data: content,
229+
onTapLink: (text, href, title) {
230+
if (href == null || href.isEmpty) return;
231+
openInAppBrowser(href);
232+
},
233+
);
234+
}),
221235
);
222236
}
223237

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import '/screens/conversations/controllers/chat.dart';
2+
import '/imports.dart';
3+
4+
class MessageActions extends StatelessWidget {
5+
final ConversationChatController controller;
6+
final MessageInfo info;
7+
8+
const MessageActions({
9+
super.key,
10+
required this.controller,
11+
required this.info,
12+
});
13+
14+
@override
15+
Widget build(BuildContext context) {
16+
return ListView(
17+
shrinkWrap: true,
18+
children: [
19+
// CustomListTile(
20+
// leading: Icon(Icons.copy_outlined),
21+
// title: Text(t.copy),
22+
// onTap: () {
23+
// Get.close();
24+
// },
25+
// ),
26+
// CustomListTile(
27+
// leading: Icon(Icons.delete_outline),
28+
// title: Text(t.delete),
29+
// onTap: () {
30+
// Get.close();
31+
// },
32+
// ),
33+
Obx(() {
34+
final translated = controller.translated.value;
35+
final isTranslating = translated.containsKey(info.id);
36+
final isTranslated =
37+
isTranslating && isNotNullOrEmpty(translated[info.id]);
38+
39+
return CustomListTile(
40+
enabled: !translated.containsKey(info.id),
41+
leading: Icon(Icons.translate),
42+
title: Text(t.translate),
43+
onTap: () async {
44+
await controller.translate(info);
45+
Get.close();
46+
},
47+
trailing: SizedBox(
48+
width: 24,
49+
height: 24,
50+
child: isTranslating && !isTranslated
51+
? CircularProgressIndicator()
52+
: null,
53+
),
54+
);
55+
}),
56+
],
57+
);
58+
}
59+
}

pubspec.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,6 +1163,14 @@ packages:
11631163
url: "https://pub.dev"
11641164
source: hosted
11651165
version: "0.10.0"
1166+
translator:
1167+
dependency: "direct main"
1168+
description:
1169+
name: translator
1170+
sha256: "8f5e56d0ffb8f493b23ad0e4f824c17e5f43d45997e33b7c7b689c7a33cf3b06"
1171+
url: "https://pub.dev"
1172+
source: hosted
1173+
version: "1.0.3+1"
11661174
typed_data:
11671175
dependency: transitive
11681176
description:

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ dependencies:
4747
just_audio: ^0.9.44 # feature-rich audio player
4848
record: ^5.2.0 # Audio recorder from microphone to a given file path or stream.
4949
flutter_colorpicker: ^1.1.0
50+
translator: ^1.0.3+1 # Free Google Translate API for Dart
5051

5152
# desktop
5253
window_manager: ^0.4.3 # This plugin allows Flutter desktop apps to resizing and repositioning the window.

0 commit comments

Comments
 (0)