Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions example/lib/views/feed/feed_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class _FeedScreenState extends State<FeedScreen> {
children: [
const SizedBox(height: 8),
LMPostWidget(
post: item,
post: PostViewData.fromPost(post: item),
isFeed: true,
user: feedResponse.users[item.userId]!,
onTagTap: (String userId) {
Expand Down Expand Up @@ -157,7 +157,7 @@ class _FeedScreenState extends State<FeedScreen> {
class MyPostWidget extends LMPostWidget {
MyPostWidget({
super.key,
required Post post,
required PostViewData post,
required User user,
required Function() onTap,
required bool isFeed,
Expand Down
3 changes: 2 additions & 1 deletion example/lib/views/post_detail/post_detail_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,8 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
child: postData == null
? const LMPostMediaShimmer()
: LMPostWidget(
post: postData!,
post: PostViewData.fromPost(
post: postData!),
onTagTap: (String userId) {
locator<LikeMindsService>()
.routeToProfile(userId);
Expand Down
3 changes: 1 addition & 2 deletions lib/likeminds_feed_ui_fl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ export 'src/widgets/widgets.dart';
export 'src/utils/typedefs.dart';
export 'src/utils/helpers.dart';
export 'src/utils/utils.dart';
export 'src/models/media_model.dart';
export 'src/models/topic_ui.dart';
export 'src/models/models.dart';
25 changes: 22 additions & 3 deletions lib/packages/expandable_text/expandable_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:likeminds_feed/likeminds_feed.dart';
import 'package:likeminds_feed_ui_fl/likeminds_feed_ui_fl.dart';
import 'package:likeminds_feed_ui_fl/packages/linkify/linkify.dart';
import 'package:likeminds_feed_ui_fl/src/utils/constants.dart';
import 'package:likeminds_feed_ui_fl/src/utils/theme.dart';
import 'package:url_launcher/url_launcher.dart';
Expand Down Expand Up @@ -412,17 +413,35 @@ class ExpandableTextState extends State<ExpandableText>
));
} else {
bool isTag = link != null && link[0] == '<';

//if it is a valid link using linkify and if that is not then add normal TextSpan
if (!isTag && extractLinkAndEmailFromString(link ?? '') == null) {
textSpans.add(TextSpan(
text: text.substring(startIndex, endIndex),
style: widget.style,
));
lastIndex = endIndex;
continue;
}
// Add a TextSpan for the URL
textSpans.add(TextSpan(
text: isTag ? TaggingHelper.decodeString(link).keys.first : link,
style: widget.linkStyle ?? const TextStyle(color: kPrimaryColor),
recognizer: TapGestureRecognizer()
..onTap = () async {
if (!isTag) {
String checkLink = getFirstValidLinkFromString(link ?? '');
if (Uri.parse(checkLink).isAbsolute) {
final checkLink = extractLinkAndEmailFromString(link ?? '');
debugPrint('checkLink: $checkLink');
if (checkLink is UrlElement) {
if (Uri.parse(checkLink.url).isAbsolute) {
launchUrl(
Uri.parse(checkLink.url),
mode: LaunchMode.externalApplication,
);
}
} else if (checkLink is EmailElement) {
launchUrl(
Uri.parse(checkLink),
Uri.parse('mailto:${checkLink.emailAddress}'),
mode: LaunchMode.externalApplication,
);
}
Expand Down
124 changes: 124 additions & 0 deletions lib/packages/linkify/linkify.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
library linkify;

import 'src/email.dart';
import 'src/url.dart';
export 'src/email.dart' show EmailLinkifier, EmailElement;
export 'src/url.dart' show UrlLinkifier, UrlElement;


abstract class LinkifyElement {
final String text;
final String originText;

LinkifyElement(this.text, [String? originText])
: originText = originText ?? text;

@override
bool operator ==(other) => equals(other);

@override
int get hashCode => Object.hash(text, originText);

bool equals(other) => other is LinkifyElement && other.text == text;
}

class LinkableElement extends LinkifyElement {
final String url;

LinkableElement(String? text, this.url, [String? originText])
: super(text ?? url, originText);

@override
bool operator ==(other) => equals(other);

@override
int get hashCode => Object.hash(text, originText, url);

@override
bool equals(other) =>
other is LinkableElement && super.equals(other) && other.url == url;
}

/// Represents an element containing text
class TextElement extends LinkifyElement {
TextElement(String text) : super(text);

@override
String toString() {
return "TextElement: '$text'";
}

@override
bool operator ==(other) => equals(other);

@override
int get hashCode => Object.hash(text, originText);

@override
bool equals(other) => other is TextElement && super.equals(other);
}

abstract class Linkifier {
const Linkifier();

List<LinkifyElement> parse(
List<LinkifyElement> elements, LinkifyOptions options);
}

class LinkifyOptions {
/// Removes http/https from shown URLs.
final bool humanize;

/// Removes www. from shown URLs.
final bool removeWww;

/// Enables loose URL parsing (any string with "." is a URL).
final bool looseUrl;

/// When used with [looseUrl], default to `https` instead of `http`.
final bool defaultToHttps;

/// Excludes `.` at end of URLs.
final bool excludeLastPeriod;

const LinkifyOptions({
this.humanize = true,
this.removeWww = false,
this.looseUrl = false,
this.defaultToHttps = false,
this.excludeLastPeriod = true,
});
}

const _urlLinkifier = UrlLinkifier();
const _emailLinkifier = EmailLinkifier();
const defaultLinkifiers = [_urlLinkifier, _emailLinkifier];

/// Turns [text] into a list of [LinkifyElement]
///
/// Use [humanize] to remove http/https from the start of the URL shown.
/// Will default to `false` (if `null`)
///
/// Uses [linkTypes] to enable some types of links (URL, email).
/// Will default to all (if `null`).
List<LinkifyElement> linkify(
String text, {
LinkifyOptions options = const LinkifyOptions(),
List<Linkifier> linkifiers = defaultLinkifiers,
}) {
var list = <LinkifyElement>[TextElement(text)];

if (text.isEmpty) {
return [];
}

if (linkifiers.isEmpty) {
return list;
}

for (var linkifier in linkifiers) {
list = linkifier.parse(list, options);
}

return list;
}
71 changes: 71 additions & 0 deletions lib/packages/linkify/src/email.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import 'package:likeminds_feed_ui_fl/packages/linkify/linkify.dart';

final _emailRegex = RegExp(
r'^(.*?)((mailto:)?[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z][A-Z]+)',
caseSensitive: false,
dotAll: true,
);

class EmailLinkifier extends Linkifier {
const EmailLinkifier();

@override
List<LinkifyElement> parse(elements, options) {
final list = <LinkifyElement>[];

for (var element in elements) {
if (element is TextElement) {
final match = _emailRegex.firstMatch(element.text);

if (match == null) {
list.add(element);
} else {
final text = element.text.replaceFirst(match.group(0)!, '');

if (match.group(1)?.isNotEmpty == true) {
list.add(TextElement(match.group(1)!));
}

if (match.group(2)?.isNotEmpty == true) {
// Always humanize emails
list.add(EmailElement(
match.group(2)!.replaceFirst(RegExp(r'mailto:'), ''),
));
}

if (text.isNotEmpty) {
list.addAll(parse([TextElement(text)], options));
}
}
} else {
list.add(element);
}
}

return list;
}
}

/// Represents an element containing an email address
class EmailElement extends LinkableElement {
final String emailAddress;

EmailElement(this.emailAddress) : super(emailAddress, 'mailto:$emailAddress');

@override
String toString() {
return "EmailElement: '$emailAddress' ($text)";
}

@override
bool operator ==(other) => equals(other);

@override
int get hashCode => Object.hash(text, originText, url, emailAddress);

@override
bool equals(other) =>
other is EmailElement &&
super.equals(other) &&
other.emailAddress == emailAddress;
}
115 changes: 115 additions & 0 deletions lib/packages/linkify/src/url.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import 'package:likeminds_feed_ui_fl/packages/linkify/linkify.dart';

final _urlRegex = RegExp(
r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[^\s]*)',
caseSensitive: false,
dotAll: true,
);

final _looseUrlRegex = RegExp(
r'''^(.*?)((https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=/'"`]*))''',
caseSensitive: false,
dotAll: true,
);


final _protocolIdentifierRegex = RegExp(
r'^(https?:\/\/)',
caseSensitive: false,
);

class UrlLinkifier extends Linkifier {
const UrlLinkifier();

@override
List<LinkifyElement> parse(elements, options) {
final list = <LinkifyElement>[];

for (var element in elements) {
if (element is TextElement) {
var match = options.looseUrl
? _looseUrlRegex.firstMatch(element.text)
: _urlRegex.firstMatch(element.text);

if (match == null) {
list.add(element);
} else {
final text = element.text.replaceFirst(match.group(0)!, '');

if (match.group(1)?.isNotEmpty == true) {
list.add(TextElement(match.group(1)!));
}

if (match.group(2)?.isNotEmpty == true) {
var originalUrl = match.group(2)!;
var originText = originalUrl;
String? end;

if ((options.excludeLastPeriod) &&
originalUrl[originalUrl.length - 1] == ".") {
end = ".";
originText = originText.substring(0, originText.length - 1);
originalUrl = originalUrl.substring(0, originalUrl.length - 1);
}

var url = originalUrl;

if (!originalUrl.startsWith(_protocolIdentifierRegex)) {
originalUrl = (options.defaultToHttps ? "https://" : "http://") +
originalUrl;
}

if ((options.humanize) || (options.removeWww)) {
if (options.humanize) {
url = url.replaceFirst(RegExp(r'https?://'), '');
}
if (options.removeWww) {
url = url.replaceFirst(RegExp(r'www\.'), '');
}

list.add(UrlElement(
originalUrl,
url,
originText,
));
} else {
list.add(UrlElement(originalUrl, null, originText));
}

if (end != null) {
list.add(TextElement(end));
}
}

if (text.isNotEmpty) {
list.addAll(parse([TextElement(text)], options));
}
}
} else {
list.add(element);
}
}

return list;
}
}

/// Represents an element containing a link
class UrlElement extends LinkableElement {
UrlElement(String url, [String? text, String? originText])
: super(text, url, originText);

@override
String toString() {
return "LinkElement: '$url' ($text)";
}

@override
bool operator ==(other) => equals(other);

@override
int get hashCode => Object.hash(text, originText, url);

@override
bool equals(other) => other is UrlElement && super.equals(other);
}
Loading