Skip to content

Commit fcf7ec9

Browse files
feat: ✨ Add support for displaying and selecting text and links in a single message view
1 parent 0d4eb7f commit fcf7ec9

File tree

5 files changed

+47
-27
lines changed

5 files changed

+47
-27
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## [3.0.0]
22

3+
* **Feat**: [374](https://github.com/SimformSolutionsPvtLtd/chatview/issues/374)
4+
Add support for displaying and selecting text and links in a single message view.
35
* **Breaking**: [411](https://github.com/SimformSolutionsPvtLtd/chatview/pull/411)
46
Update the example iOS deployment target to 13, as the example depends on `audiowaveform`, which requires iOS 13 or later.
57
* **Feat**: [401](https://github.com/SimformSolutionsPvtLtd/chatview/pull/401)

lib/src/extensions/extensions.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ extension ValidateString on String {
112112

113113
bool get isUrl => Uri.tryParse(this)?.isAbsolute ?? false;
114114

115+
/// Regular expression pattern to match URLs.
116+
static final _urlRegex = RegExp(urlRegex, caseSensitive: false);
117+
118+
/// Extracts the first URL found in the string.
119+
String get extractedUrl => _urlRegex.firstMatch(this)?.group(0) ?? '';
120+
115121
Widget getUserProfilePicture({
116122
required ChatUser? Function(String) getChatUser,
117123
double? profileCircleRadius,

lib/src/utils/constants/constants.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ applicationDateFormatter(DateTime inputTime) {
6565
}
6666
}
6767

68+
/// Regular expression to identify URLs in a text.
69+
const String urlRegex =
70+
r'((https?://)?(www\.)?[a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b'
71+
r'([-a-zA-Z0-9@:%_\+.~#?&//=]*))';
72+
6873
/// Default widget that appears on receipts at [MessageStatus.pending] when a message
6974
/// is not sent or at the pending state. A custom implementation can have different
7075
/// widgets for different states.

lib/src/widgets/link_preview.dart

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,31 +30,36 @@ import '../utils/constants/constants.dart';
3030
class LinkPreview extends StatelessWidget {
3131
const LinkPreview({
3232
Key? key,
33-
required this.url,
33+
required this.textMessage,
34+
required this.extractedUrl,
3435
this.linkPreviewConfig,
3536
}) : super(key: key);
3637

38+
/// Provides the whole text message to show.
39+
final String textMessage;
40+
3741
/// Provides url which is passed in message.
38-
final String url;
42+
final String extractedUrl;
3943

4044
/// Provides configuration of chat bubble appearance when link/URL is passed
4145
/// in message.
4246
final LinkPreviewConfiguration? linkPreviewConfig;
4347

4448
@override
4549
Widget build(BuildContext context) {
50+
final isImageUrl = extractedUrl.isImageUrl;
4651
return Padding(
4752
padding: linkPreviewConfig?.padding ??
4853
const EdgeInsets.symmetric(horizontal: 6, vertical: verticalPadding),
4954
child: Column(
5055
crossAxisAlignment: CrossAxisAlignment.start,
5156
children: [
52-
if (!url.isImageUrl &&
57+
if (!isImageUrl &&
5358
!(context.chatBubbleConfig?.disableLinkPreview ?? false)) ...{
5459
Padding(
5560
padding: const EdgeInsets.symmetric(vertical: verticalPadding),
5661
child: AnyLinkPreview(
57-
link: url,
62+
link: extractedUrl,
5863
removeElevation: true,
5964
errorBody: linkPreviewConfig?.errorBody,
6065
proxyUrl: linkPreviewConfig?.proxyUrl,
@@ -77,13 +82,13 @@ class LinkPreview extends StatelessWidget {
7782
titleStyle: linkPreviewConfig?.titleStyle,
7883
),
7984
),
80-
} else if (url.isImageUrl) ...{
85+
} else if (isImageUrl) ...{
8186
Padding(
8287
padding: const EdgeInsets.symmetric(vertical: verticalPadding),
8388
child: InkWell(
8489
onTap: _onLinkTap,
8590
child: Image.network(
86-
url,
91+
extractedUrl,
8792
height: 120,
8893
width: double.infinity,
8994
fit: BoxFit.fitWidth,
@@ -95,7 +100,7 @@ class LinkPreview extends StatelessWidget {
95100
InkWell(
96101
onTap: _onLinkTap,
97102
child: Text(
98-
url,
103+
textMessage,
99104
style: linkPreviewConfig?.linkStyle ??
100105
const TextStyle(
101106
color: Colors.white,
@@ -110,14 +115,14 @@ class LinkPreview extends StatelessWidget {
110115

111116
void _onLinkTap() {
112117
if (linkPreviewConfig?.onUrlDetect != null) {
113-
linkPreviewConfig?.onUrlDetect!(url);
118+
linkPreviewConfig?.onUrlDetect!(extractedUrl);
114119
} else {
115120
_launchURL();
116121
}
117122
}
118123

119124
void _launchURL() async {
120-
final parsedUrl = Uri.parse(url);
125+
final parsedUrl = Uri.parse(extractedUrl);
121126
await canLaunchUrl(parsedUrl)
122127
? await launchUrl(parsedUrl)
123128
: throw couldNotLaunch;

lib/src/widgets/text_message_view.dart

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,21 @@ class TextMessageView extends StatelessWidget {
7878
final textSelectionConfig = isMessageBySender
7979
? outgoingChatBubbleConfig?.textSelectionConfig
8080
: inComingChatBubbleConfig?.textSelectionConfig;
81-
final baseWidget = Text(
82-
textMessage,
83-
style: _textStyle ??
84-
textTheme.bodyMedium!.copyWith(
85-
color: Colors.white,
86-
fontSize: 16,
87-
),
88-
);
81+
final extractedUrl = textMessage.extractedUrl;
82+
final baseWidget = extractedUrl.isNotEmpty
83+
? LinkPreview(
84+
linkPreviewConfig: _linkPreviewConfig,
85+
textMessage: textMessage,
86+
extractedUrl: extractedUrl,
87+
)
88+
: Text(
89+
textMessage,
90+
style: _textStyle ??
91+
textTheme.bodyMedium!.copyWith(
92+
color: Colors.white,
93+
fontSize: 16,
94+
),
95+
);
8996
return Stack(
9097
clipBehavior: Clip.none,
9198
children: [
@@ -106,17 +113,12 @@ class TextMessageView extends StatelessWidget {
106113
border: border,
107114
borderRadius: _borderRadius(textMessage),
108115
),
109-
child: textMessage.isUrl
110-
? LinkPreview(
111-
linkPreviewConfig: _linkPreviewConfig,
112-
url: textMessage,
116+
child: isSelectable
117+
? CustomSelectionArea(
118+
config: textSelectionConfig,
119+
child: baseWidget,
113120
)
114-
: isSelectable
115-
? CustomSelectionArea(
116-
config: textSelectionConfig,
117-
child: baseWidget,
118-
)
119-
: baseWidget,
121+
: baseWidget,
120122
),
121123
if (message.reaction.reactions.isNotEmpty)
122124
ReactionWidget(

0 commit comments

Comments
 (0)