Skip to content

Commit c56dff9

Browse files
Merge pull request #58 from ChandruWritesCode/35-markdown-support-in-messages
#35 markdown support in messages
2 parents 6578672 + c41701e commit c56dff9

3 files changed

Lines changed: 214 additions & 70 deletions

File tree

mobile/lib/widgets/chat_screen_modular_widgets.dart

Lines changed: 197 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22
import 'dart:ui';
33
import 'package:flutter/material.dart';
4+
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
45

56
class GlassAppBar extends StatelessWidget implements PreferredSizeWidget {
67
final String name;
@@ -104,11 +105,35 @@ class ChatBubble extends StatelessWidget {
104105
crossAxisAlignment: CrossAxisAlignment.end,
105106
mainAxisSize: MainAxisSize.min,
106107
children: [
107-
Text(
108-
message,
109-
style: TextStyle(
110-
color: isMe ? Colors.white : Colors.black87,
111-
fontSize: 15,
108+
MarkdownBody(
109+
data: message,
110+
selectable: false,
111+
styleSheet: MarkdownStyleSheet(
112+
p: TextStyle(
113+
color: isMe ? Colors.white : Colors.black87,
114+
fontSize: 15,
115+
),
116+
strong: TextStyle(
117+
color: isMe ? Colors.white : Colors.black87,
118+
fontWeight: FontWeight.bold,
119+
),
120+
em: TextStyle(
121+
color: isMe ? Colors.white : Colors.black87,
122+
fontStyle: FontStyle.italic,
123+
),
124+
del: TextStyle(
125+
color: isMe ? Colors.white70 : Colors.black54,
126+
decoration: TextDecoration.lineThrough,
127+
),
128+
code: TextStyle(
129+
backgroundColor: isMe ? Colors.blue[800] : Colors.grey[300],
130+
color: isMe ? Colors.blue[50] : Colors.red[800],
131+
fontFamily: 'monospace',
132+
),
133+
a: TextStyle(
134+
color: isMe ? Colors.blue[100] : Colors.blue[700],
135+
decoration: TextDecoration.underline,
136+
),
112137
),
113138
),
114139
const SizedBox(height: 4),
@@ -155,8 +180,21 @@ class ChatInputArea extends StatefulWidget {
155180

156181
class _ChatInputAreaState extends State<ChatInputArea> {
157182
final _controller = TextEditingController();
183+
final FocusNode _focusNode = FocusNode();
184+
158185
bool _isTyping = false;
159186
Timer? _typingTimer;
187+
bool _showFormattingToolbar = false;
188+
189+
@override
190+
void initState() {
191+
super.initState();
192+
_focusNode.addListener(() {
193+
setState(() {
194+
_showFormattingToolbar = _focusNode.hasFocus;
195+
});
196+
});
197+
}
160198

161199
@override
162200
void dispose() {
@@ -165,6 +203,7 @@ class _ChatInputAreaState extends State<ChatInputArea> {
165203
widget.onTypingChanged?.call(false);
166204
}
167205
_controller.dispose();
206+
_focusNode.dispose();
168207
super.dispose();
169208
}
170209

@@ -203,6 +242,35 @@ class _ChatInputAreaState extends State<ChatInputArea> {
203242
}
204243
}
205244

245+
void _insertMarkdown(String prefix, String suffix) {
246+
final text = _controller.text;
247+
final selection = _controller.selection;
248+
249+
if (!selection.isValid || selection.start == selection.end) {
250+
final newText = text + prefix + suffix;
251+
_controller.value = TextEditingValue(
252+
text: newText,
253+
selection: TextSelection.collapsed(
254+
offset: newText.length - suffix.length,
255+
),
256+
);
257+
} else {
258+
final selectedText = text.substring(selection.start, selection.end);
259+
final newText = text.replaceRange(
260+
selection.start,
261+
selection.end,
262+
'$prefix$selectedText$suffix',
263+
);
264+
_controller.value = TextEditingValue(
265+
text: newText,
266+
selection: TextSelection.collapsed(
267+
offset: selection.start + prefix.length + selectedText.length,
268+
),
269+
);
270+
}
271+
_handleOnChange(_controller.text);
272+
}
273+
206274
void _submit() {
207275
_isOverLimit = false;
208276
final text = _controller.text.trim();
@@ -230,74 +298,115 @@ class _ChatInputAreaState extends State<ChatInputArea> {
230298
child: Container(
231299
color: Colors.transparent,
232300
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 20),
233-
child: Row(
234-
mainAxisAlignment: MainAxisAlignment.center,
235-
crossAxisAlignment: CrossAxisAlignment.start,
301+
child: Column(
302+
mainAxisSize: MainAxisSize.min,
236303
children: [
237-
Expanded(
238-
child: TextField(
239-
controller: _controller,
240-
onChanged: _handleOnChange,
241-
keyboardType: TextInputType.multiline,
242-
minLines: 1,
243-
maxLines: 5,
244-
maxLength: _charLimit,
245-
maxLengthEnforcement: .none,
246-
// cursorColor: Colors.blue,
247-
style: TextStyle(
248-
color: _isOverLimit ? Colors.red : Colors.black87,
249-
fontSize: 15,
250-
),
251-
decoration: InputDecoration(
252-
hintText: "Write a message...",
253-
hintStyle: const TextStyle(color: Colors.black38),
254-
255-
fillColor: _isOverLimit
256-
? Colors.red.withValues(alpha: 0.1)
257-
: const Color(0xFFF5F5F5),
258-
filled: true,
259-
errorText: _isOverLimit
260-
? 'character limit exceeded'
261-
: null,
262-
counterStyle: TextStyle(
263-
color: _isOverLimit ? Colors.red : Colors.black54,
264-
fontWeight: _isOverLimit
265-
? FontWeight.bold
266-
: FontWeight.normal,
267-
),
268-
269-
border: OutlineInputBorder(
270-
borderRadius: BorderRadius.circular(24),
271-
borderSide: _isOverLimit
272-
? const BorderSide(color: Colors.red, width: 1.5)
273-
: BorderSide.none,
274-
),
275-
focusedBorder: OutlineInputBorder(
276-
borderRadius: BorderRadius.circular(24),
277-
borderSide: _isOverLimit
278-
? const BorderSide(color: Colors.red, width: 2)
279-
: BorderSide.none,
280-
),
281-
enabledBorder: OutlineInputBorder(
282-
borderRadius: BorderRadius.circular(24),
283-
borderSide: _isOverLimit
284-
? const BorderSide(color: Colors.red, width: 1.5)
285-
: BorderSide.none,
286-
),
287-
288-
contentPadding: const EdgeInsets.symmetric(
289-
horizontal: 16,
290-
vertical: 10,
304+
AnimatedSize(
305+
duration: const Duration(milliseconds: 200),
306+
child: _showFormattingToolbar
307+
? Container(
308+
padding: const EdgeInsets.only(bottom: 8),
309+
child: Row(
310+
mainAxisAlignment: MainAxisAlignment.start,
311+
children: [
312+
_FormatButton(
313+
icon: Icons.format_bold,
314+
onPressed: () => _insertMarkdown('**', '**'),
315+
),
316+
_FormatButton(
317+
icon: Icons.format_italic,
318+
onPressed: () => _insertMarkdown('*', '*'),
319+
),
320+
_FormatButton(
321+
icon: Icons.format_strikethrough,
322+
onPressed: () => _insertMarkdown('~~', '~~'),
323+
),
324+
_FormatButton(
325+
icon: Icons.code,
326+
onPressed: () => _insertMarkdown('`', '`'),
327+
),
328+
_FormatButton(
329+
icon: Icons.link,
330+
onPressed: () => _insertMarkdown('[', '](url)'),
331+
),
332+
],
333+
),
334+
)
335+
: const SizedBox.shrink(),
336+
),
337+
Row(
338+
mainAxisAlignment: MainAxisAlignment.center,
339+
crossAxisAlignment: CrossAxisAlignment.start,
340+
children: [
341+
Expanded(
342+
child: TextField(
343+
controller: _controller,
344+
focusNode: _focusNode,
345+
onChanged: _handleOnChange,
346+
keyboardType: TextInputType.multiline,
347+
minLines: 1,
348+
maxLines: 5,
349+
maxLength: _charLimit,
350+
maxLengthEnforcement: .none,
351+
style: TextStyle(
352+
color: _isOverLimit ? Colors.red : Colors.black87,
353+
fontSize: 15,
354+
),
355+
decoration: InputDecoration(
356+
hintText: "Write a message...",
357+
hintStyle: const TextStyle(color: Colors.black38),
358+
fillColor: _isOverLimit
359+
? Colors.red.withValues(alpha: 0.1)
360+
: const Color(0xFFF5F5F5),
361+
filled: true,
362+
errorText: _isOverLimit
363+
? 'character limit exceeded'
364+
: null,
365+
counterStyle: TextStyle(
366+
color: _isOverLimit ? Colors.red : Colors.black54,
367+
fontWeight: _isOverLimit
368+
? FontWeight.bold
369+
: FontWeight.normal,
370+
),
371+
border: OutlineInputBorder(
372+
borderRadius: BorderRadius.circular(24),
373+
borderSide: _isOverLimit
374+
? const BorderSide(
375+
color: Colors.red,
376+
width: 1.5,
377+
)
378+
: BorderSide.none,
379+
),
380+
focusedBorder: OutlineInputBorder(
381+
borderRadius: BorderRadius.circular(24),
382+
borderSide: _isOverLimit
383+
? const BorderSide(color: Colors.red, width: 2)
384+
: BorderSide.none,
385+
),
386+
enabledBorder: OutlineInputBorder(
387+
borderRadius: BorderRadius.circular(24),
388+
borderSide: _isOverLimit
389+
? const BorderSide(
390+
color: Colors.red,
391+
width: 1.5,
392+
)
393+
: BorderSide.none,
394+
),
395+
contentPadding: const EdgeInsets.symmetric(
396+
horizontal: 16,
397+
vertical: 10,
398+
),
399+
),
291400
),
292401
),
293-
),
294-
),
295-
const SizedBox(width: 8),
296-
IconButton(
297-
icon: _isOverLimit
298-
? const Icon(Icons.not_interested)
299-
: const Icon(Icons.send, color: Color(0xFF1890FF)),
300-
onPressed: _isOverLimit ? null : _submit,
402+
const SizedBox(width: 8),
403+
IconButton(
404+
icon: _isOverLimit
405+
? const Icon(Icons.not_interested)
406+
: const Icon(Icons.send, color: Color(0xFF1890FF)),
407+
onPressed: _isOverLimit ? null : _submit,
408+
),
409+
],
301410
),
302411
],
303412
),
@@ -307,3 +416,21 @@ class _ChatInputAreaState extends State<ChatInputArea> {
307416
);
308417
}
309418
}
419+
420+
class _FormatButton extends StatelessWidget {
421+
final IconData icon;
422+
final VoidCallback onPressed;
423+
424+
const _FormatButton({required this.icon, required this.onPressed});
425+
426+
@override
427+
Widget build(BuildContext context) {
428+
return IconButton(
429+
icon: Icon(icon, size: 20, color: Colors.black54),
430+
onPressed: onPressed,
431+
visualDensity: VisualDensity.compact,
432+
padding: const EdgeInsets.symmetric(horizontal: 4),
433+
constraints: const BoxConstraints(),
434+
);
435+
}
436+
}

mobile/pubspec.lock

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,14 @@ packages:
262262
url: "https://pub.dev"
263263
source: hosted
264264
version: "6.0.0"
265+
flutter_markdown_plus:
266+
dependency: "direct main"
267+
description:
268+
name: flutter_markdown_plus
269+
sha256: "039177906850278e8fb1cd364115ee0a46281135932fa8ecea8455522166d2de"
270+
url: "https://pub.dev"
271+
source: hosted
272+
version: "1.0.7"
265273
flutter_plugin_android_lifecycle:
266274
dependency: transitive
267275
description:
@@ -536,6 +544,14 @@ packages:
536544
url: "https://pub.dev"
537545
source: hosted
538546
version: "1.3.0"
547+
markdown:
548+
dependency: transitive
549+
description:
550+
name: markdown
551+
sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9
552+
url: "https://pub.dev"
553+
source: hosted
554+
version: "7.3.1"
539555
matcher:
540556
dependency: transitive
541557
description:

mobile/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ dependencies:
4848
flutter_chat_types: ^3.6.2
4949
flutter_secure_storage: ^10.3.1
5050
dio: ^5.10.0
51+
flutter_markdown_plus: ^1.0.7
5152

5253
launcher_name:
5354
default: "Elephant"

0 commit comments

Comments
 (0)