Skip to content

Commit 288651e

Browse files
japanshah-simformSahil-Simform
authored andcommitted
feat: ✨ Enhance link preview to support multiple URLs extraction and clickable links.
1 parent 6cbdeb6 commit 288651e

File tree

5 files changed

+132
-28
lines changed

5 files changed

+132
-28
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## [3.0.0]
2-
2+
* **Feat**: [414](https://github.com/SimformSolutionsPvtLtd/chatview/pull/414)
3+
Enhance link preview to support multiple URLs extraction and clickable links.
34
* **Feat**: [374](https://github.com/SimformSolutionsPvtLtd/chatview/issues/374)
45
Add support for displaying and selecting text and links in a single message view.
56
* **Breaking**: [411](https://github.com/SimformSolutionsPvtLtd/chatview/pull/411)

lib/src/extensions/extensions.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,10 @@ extension ValidateString on String {
115115
/// Regular expression pattern to match URLs.
116116
static final _urlRegex = RegExp(urlRegex, caseSensitive: false);
117117

118-
/// Extracts the first URL found in the string.
119-
String? get extractedUrl => _urlRegex.firstMatch(this)?.group(0);
118+
/// Extracts all URLs from the string and returns them as a list.
119+
List<String> get extractedUrls {
120+
return _urlRegex.allMatches(this).map((m) => m.group(0)!).toList();
121+
}
120122

121123
Widget getUserProfilePicture({
122124
required ChatUser? Function(String) getChatUser,

lib/src/widgets/link_preview.dart

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,28 +26,34 @@ import 'package:url_launcher/url_launcher.dart';
2626
import '../extensions/extensions.dart';
2727
import '../models/config_models/link_preview_configuration.dart';
2828
import '../utils/constants/constants.dart';
29+
import 'link_preview_text.dart';
2930

3031
class LinkPreview extends StatelessWidget {
3132
const LinkPreview({
3233
Key? key,
3334
required this.textMessage,
34-
required this.extractedUrl,
35+
required this.extractedUrls,
3536
this.linkPreviewConfig,
37+
required this.normalTextStyle,
3638
}) : super(key: key);
3739

3840
/// Provides the whole text message to show.
3941
final String textMessage;
4042

4143
/// Provides url which is passed in message.
42-
final String extractedUrl;
44+
final List<String> extractedUrls;
4345

4446
/// Provides configuration of chat bubble appearance when link/URL is passed
4547
/// in message.
4648
final LinkPreviewConfiguration? linkPreviewConfig;
4749

50+
/// Provides normal text style for message text.
51+
final TextStyle normalTextStyle;
52+
4853
@override
4954
Widget build(BuildContext context) {
50-
final isImageUrl = extractedUrl.isImageUrl;
55+
final firstUrl = extractedUrls.first;
56+
final isImageUrl = firstUrl.isImageUrl;
5157
return Padding(
5258
padding: linkPreviewConfig?.padding ??
5359
const EdgeInsets.symmetric(horizontal: 6, vertical: verticalPadding),
@@ -59,11 +65,11 @@ class LinkPreview extends StatelessWidget {
5965
Padding(
6066
padding: const EdgeInsets.symmetric(vertical: verticalPadding),
6167
child: AnyLinkPreview(
62-
link: extractedUrl,
68+
link: firstUrl,
6369
removeElevation: true,
6470
errorBody: linkPreviewConfig?.errorBody,
6571
proxyUrl: linkPreviewConfig?.proxyUrl,
66-
onTap: _onLinkTap,
72+
onTap: () => _onLinkTap(firstUrl),
6773
placeholderWidget: SizedBox(
6874
height: MediaQuery.of(context).size.height * 0.25,
6975
width: double.infinity,
@@ -86,9 +92,9 @@ class LinkPreview extends StatelessWidget {
8692
Padding(
8793
padding: const EdgeInsets.symmetric(vertical: verticalPadding),
8894
child: InkWell(
89-
onTap: _onLinkTap,
95+
onTap: () => _onLinkTap(firstUrl),
9096
child: Image.network(
91-
extractedUrl,
97+
firstUrl,
9298
height: 120,
9399
width: double.infinity,
94100
fit: BoxFit.fitWidth,
@@ -97,32 +103,28 @@ class LinkPreview extends StatelessWidget {
97103
),
98104
},
99105
const SizedBox(height: verticalPadding),
100-
InkWell(
101-
onTap: _onLinkTap,
102-
child: Text(
103-
textMessage,
104-
style: linkPreviewConfig?.linkStyle ??
105-
const TextStyle(
106-
color: Colors.white,
107-
decoration: TextDecoration.underline,
108-
),
109-
),
106+
LinkPreviewText(
107+
textMessage: textMessage,
108+
extractedUrls: extractedUrls,
109+
linkPreviewConfig: linkPreviewConfig,
110+
onLinkTap: _onLinkTap,
111+
normalTextStyle: normalTextStyle,
110112
),
111113
],
112114
),
113115
);
114116
}
115117

116-
void _onLinkTap() {
118+
void _onLinkTap(String url) {
117119
if (linkPreviewConfig?.onUrlDetect case final onUrlDetect?) {
118-
onUrlDetect(extractedUrl);
120+
onUrlDetect(url);
119121
} else {
120-
_launchURL();
122+
_launchURL(url);
121123
}
122124
}
123125

124-
void _launchURL() async {
125-
final parsedUrl = Uri.parse(extractedUrl);
126+
void _launchURL(String url) async {
127+
final parsedUrl = Uri.parse(url);
126128
await canLaunchUrl(parsedUrl)
127129
? await launchUrl(parsedUrl)
128130
: throw couldNotLaunch;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import 'package:flutter/gestures.dart';
2+
import 'package:flutter/material.dart';
3+
4+
import '../../chatview.dart';
5+
6+
class LinkPreviewText extends StatefulWidget {
7+
const LinkPreviewText({
8+
super.key,
9+
required this.textMessage,
10+
required this.extractedUrls,
11+
required this.linkPreviewConfig,
12+
required this.onLinkTap,
13+
required this.normalTextStyle,
14+
});
15+
16+
/// Provides the whole text message to show.
17+
final String textMessage;
18+
19+
/// Provides urls which are passed in message.
20+
final List<String> extractedUrls;
21+
22+
/// Provides configuration of chat bubble appearance when link/URL is passed
23+
final LinkPreviewConfiguration? linkPreviewConfig;
24+
25+
/// Callback when a link is tapped.
26+
final ValueSetter<String> onLinkTap;
27+
28+
/// Provides normal text style for message text.
29+
final TextStyle normalTextStyle;
30+
31+
@override
32+
State<LinkPreviewText> createState() => _LinkPreviewTextState();
33+
}
34+
35+
class _LinkPreviewTextState extends State<LinkPreviewText> {
36+
final List<TapGestureRecognizer> _recognizers = [];
37+
38+
@override
39+
Widget build(BuildContext context) {
40+
final linkStyle = widget.linkPreviewConfig?.linkStyle;
41+
final spans = <TextSpan>[];
42+
int lastIndex = 0;
43+
44+
for (final url in widget.extractedUrls) {
45+
final start = widget.textMessage.indexOf(url, lastIndex);
46+
if (start == -1) continue;
47+
48+
if (start > lastIndex) {
49+
spans.add(
50+
TextSpan(
51+
text: widget.textMessage.substring(lastIndex, start),
52+
style: widget.normalTextStyle,
53+
),
54+
);
55+
}
56+
57+
final recognizer = TapGestureRecognizer()
58+
..onTap = () => widget.onLinkTap(url);
59+
_recognizers.add(recognizer);
60+
61+
spans.add(
62+
TextSpan(
63+
text: url,
64+
style: linkStyle,
65+
recognizer: recognizer,
66+
),
67+
);
68+
69+
lastIndex = start + url.length;
70+
}
71+
72+
if (lastIndex < widget.textMessage.length) {
73+
spans.add(
74+
TextSpan(
75+
text: widget.textMessage.substring(lastIndex),
76+
style: widget.normalTextStyle,
77+
),
78+
);
79+
}
80+
81+
return RichText(
82+
text: TextSpan(children: spans),
83+
);
84+
}
85+
86+
@override
87+
void dispose() {
88+
for (final recognizer in _recognizers) {
89+
recognizer.dispose();
90+
}
91+
super.dispose();
92+
}
93+
}

lib/src/widgets/text_message_view.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class TextMessageView extends StatelessWidget {
6565
/// Allow user to set color of highlighted message.
6666
final Color? highlightColor;
6767

68+
/// Provides configuration of active features in chat.
6869
final FeatureActiveConfig? featureActiveConfig;
6970

7071
@override
@@ -78,12 +79,17 @@ class TextMessageView extends StatelessWidget {
7879
final textSelectionConfig = isMessageBySender
7980
? outgoingChatBubbleConfig?.textSelectionConfig
8081
: inComingChatBubbleConfig?.textSelectionConfig;
81-
final extractedUrl = textMessage.extractedUrl;
82-
final baseWidget = extractedUrl != null
82+
final extractedUrls = textMessage.extractedUrls;
83+
final baseWidget = extractedUrls.isNotEmpty
8384
? LinkPreview(
8485
linkPreviewConfig: _linkPreviewConfig,
8586
textMessage: textMessage,
86-
extractedUrl: extractedUrl,
87+
extractedUrls: extractedUrls,
88+
normalTextStyle: _textStyle ??
89+
textTheme.bodyMedium!.copyWith(
90+
color: Colors.white,
91+
fontSize: 16,
92+
),
8793
)
8894
: Text(
8995
textMessage,

0 commit comments

Comments
 (0)