Skip to content

Commit

Permalink
feat: add support for clickable links in code viewer
Browse files Browse the repository at this point in the history
  • Loading branch information
jacob-i committed Mar 12, 2024
1 parent d22e766 commit dde7bbd
Showing 1 changed file with 141 additions and 12 deletions.
153 changes: 141 additions & 12 deletions app/lib/views/widgets/message_bubble.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_highlight/flutter_highlight.dart';
import 'package:flutter_highlight/themes/github.dart';
import 'package:flutter/services.dart';

class MessageBubble extends StatelessWidget {
final String message;
Expand All @@ -11,8 +15,8 @@ class MessageBubble extends StatelessWidget {
{super.key});

// Launches the link if it is valid
void _launchLink() async {
final uri = Uri.parse(link!);
void _launchLink({String l = ""}) async {
final uri = Uri.parse(l == "" ? link! : l);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
Expand Down Expand Up @@ -77,7 +81,76 @@ class MessageBubble extends StatelessWidget {
// If the link does not result in a valid image, the image widget will handle the error and may fall back to a placeholder or error widget.
Widget _buildMessageOrLink(BuildContext context) {
if (link == null) {
return _buildMessageText(context);
if (message.contains('```')) {
// Split the message by the code block
final parts = message.split('```');
final normalText = parts[0];
String codeBlock = parts.length > 1 ? parts[1] : '';
String language = 'plaintext';

// Check if there's a specified language
final codeParts = codeBlock.split('\n');
if (codeParts.length > 1 && codeParts[0].trim().isNotEmpty) {
language = codeParts[0].trim(); // The first line is the language
codeBlock = codeParts.sublist(1).join('\n'); // The rest is the code
}

return Column(
crossAxisAlignment:
isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
// Display normal text as selectable
SelectableText(
normalText,
style: TextStyle(color: isMe ? Colors.black : Colors.white70),
textAlign: isMe ? TextAlign.end : TextAlign.start,
),
// Display code block with syntax highlighting
if (codeBlock.isNotEmpty)
Container(
padding: const EdgeInsets.all(8.0),
color: Colors
.grey[200], // Light grey background for the code block
child: Column(
children: [
HighlightView(
codeBlock
.trim(), // Trim the code block to remove leading/trailing whitespace
language: language, // Specify the language
theme:
githubTheme, // Specify the theme for syntax highlighting
textStyle: const TextStyle(
fontFamily:
'monospace'), // Optional: specify text style
),
// Copy Code button
TextButton(
onPressed: () {
Clipboard.setData(
ClipboardData(text: codeBlock.trim()));
// Optionally, show a snackbar or toast to indicate that the code has been copied
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Code copied to clipboard!')),
);
},
child: const Text('Copy Code'),
),
],
),
),
// Display the rest of the message as selectable, if any
if (parts.length > 2)
SelectableText(
parts.sublist(2).join('```'),
style: TextStyle(color: isMe ? Colors.black : Colors.white70),
textAlign: isMe ? TextAlign.end : TextAlign.start,
),
],
);
} else {
return _buildMessageText(context);
}
} else if (message == 'image') {
return Image.network(
link!,
Expand All @@ -93,20 +166,76 @@ class MessageBubble extends StatelessWidget {

// Builds the message text with the appropriate color and alignment
Widget _buildMessageText(BuildContext context) {
return SelectableText.rich(
TextSpan(
children: [
TextSpan(
text: message,
style: TextStyle(color: isMe ? Colors.black : Colors.white70),
// Split the message by '**' to identify bold sections
final parts = message.split('**');
List<TextSpan> spans = [];

// Regular expression to match URLs
final urlRegex = RegExp(r'https?:\/\/[^\s)]+', caseSensitive: false);

// Iterate over the parts and apply bold style to every second element
for (int i = 0; i < parts.length; i++) {
final part = parts[i];
// Check if the part contains a URL
if (urlRegex.hasMatch(part)) {
final matches = urlRegex.allMatches(part);
int lastMatchEnd = 0;

for (var match in matches) {
// Add text before the URL
spans.add(TextSpan(
text: part.substring(lastMatchEnd, match.start),
style: TextStyle(
color: isMe ? Colors.black : Colors.white70,
fontWeight: i % 2 == 1 ? FontWeight.bold : FontWeight.normal,
),
));

// Add the URL with a link style and gesture recognizer
spans.add(TextSpan(
text: part.substring(match.start, match.end),
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
_launchLink(l: part.substring(match.start, match.end));
},
));

lastMatchEnd = match.end;
}

// Add any remaining text after the last URL
if (lastMatchEnd < part.length) {
spans.add(TextSpan(
text: part.substring(lastMatchEnd),
style: TextStyle(
color: isMe ? Colors.black : Colors.white70,
fontWeight: i % 2 == 1 ? FontWeight.bold : FontWeight.normal,
),
));
}
} else {
spans.add(TextSpan(
text: part,
style: TextStyle(
color: isMe ? Colors.black : Colors.white70,
fontWeight: i % 2 == 1 ? FontWeight.bold : FontWeight.normal,
),
// Add more TextSpans if needed for different styles within the message
],
),
));
}
}

return SelectableText.rich(
TextSpan(children: spans),
textAlign: TextAlign.start,
);
}

// Existing _launchLink method can be used here

// Builds the link button, which launches the link when pressed
ElevatedButton _buildLinkButton(BuildContext context) {
return ElevatedButton(
Expand Down

0 comments on commit dde7bbd

Please sign in to comment.