Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TW-2114: Display phone number as a clickable in message bubble #2120

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
10 changes: 6 additions & 4 deletions assets/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3084,7 +3084,7 @@
"sSwitchCameraLensDirectionLabel": "Switch to the {value} camera",
"photo": "Photo",
"video": "Video",
"message":"Message",
"message": "Message",
"fileTooBig": "The selected file is too large. Please choose a file smaller than {maxSize} MB.",
"@fileTooBig": {
"placeholders": {
Expand All @@ -3093,7 +3093,9 @@
}
}
},
"enable_notifications":"Enable notifications",
"disable_notifications":"Disable notifications",
"logoutDialogWarning": "You will lose access to encrypted messages. We recommend that you enable chat backups before logging out"
"enable_notifications": "Enable notifications",
"disable_notifications": "Disable notifications",
"logoutDialogWarning": "You will lose access to encrypted messages. We recommend that you enable chat backups before logging out",
"copyNumber": "Copy number",
"callViaCarrier": "Call via Carrier"
}
16 changes: 13 additions & 3 deletions lib/pages/chat/events/html_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@ import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/domain/model/extensions/string_extension.dart';
import 'package:fluffychat/pages/image_viewer/image_viewer.dart';
import 'package:fluffychat/presentation/mixins/linkify_mixin.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/mentioned_user.dart';
import 'package:flutter/material.dart';

import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_matrix_html/flutter_html.dart';
import 'package:linkfy_text/linkfy_text.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/presentation/extensions/send_file_extension.dart';

import 'package:fluffychat/widgets/matrix.dart';

class HtmlMessage extends StatelessWidget {
class HtmlMessage extends StatelessWidget with LinkifyMixin {
final String html;
final int? maxLines;
final Room room;
Expand All @@ -23,7 +25,7 @@ class HtmlMessage extends StatelessWidget {
final double? emoteSize;
final Widget? bottomWidgetSpan;

const HtmlMessage({
HtmlMessage({
super.key,
required this.html,
this.maxLines,
Expand Down Expand Up @@ -70,9 +72,17 @@ class HtmlMessage extends StatelessWidget {
decoration: TextDecoration.underline,
decorationColor: themeData.colorScheme.secondary,
),
linkTypes: const [
LinkType.url,
LinkType.phone,
],
shrinkToFit: true,
maxLines: maxLines,
onLinkTap: (url) => UrlLauncher(context, url: url.toString()).launchUrl(),
onTapDownLink: (tapDownDetails, link) => handleOnTappedLinkHtml(
context: context,
details: tapDownDetails,
link: link,
),
onPillTap: !room.isDirectChat
? (url) {
UrlLauncher(context, url: url, room: room).launchUrl();
Expand Down
25 changes: 25 additions & 0 deletions lib/pages/chat/phone_number_context_menu_actions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';

enum PhoneNumberContextMenuActions {
call,
copy;

String getTitle(BuildContext context) {
switch (this) {
case PhoneNumberContextMenuActions.call:
return L10n.of(context)!.callViaCarrier;
case PhoneNumberContextMenuActions.copy:
return L10n.of(context)!.copyNumber;
}
}

IconData getIconData() {
switch (this) {
case PhoneNumberContextMenuActions.call:
return Icons.call_outlined;
case PhoneNumberContextMenuActions.copy:
return Icons.content_copy_outlined;
}
}
}
190 changes: 190 additions & 0 deletions lib/presentation/mixins/linkify_mixin.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import 'package:fluffychat/pages/chat/phone_number_context_menu_actions.dart';
import 'package:fluffychat/utils/extension/build_context_extension.dart';
import 'package:fluffychat/utils/extension/value_notifier_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/context_menu/context_menu_action.dart';
import 'package:fluffychat/widgets/context_menu/twake_context_menu.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter/services.dart';
import 'package:linagora_design_flutter/linagora_design_flutter.dart';
import 'package:linkfy_text/linkfy_text.dart';
import 'package:matrix/matrix.dart';
import 'package:pull_down_button/pull_down_button.dart';
import 'package:url_launcher/url_launcher.dart';

mixin LinkifyMixin {
final ValueNotifier<bool> openingPopupMenu = ValueNotifier(false);

List<PhoneNumberContextMenuActions> get phoneNumberContextMenuOnWeb => [
PhoneNumberContextMenuActions.copy,
];

List<ContextMenuAction> _mapPopupMenuActionsToContextMenuActions({
required BuildContext context,
required List<PhoneNumberContextMenuActions> actions,
}) {
return actions.map((action) {
return ContextMenuAction(
name: action.getTitle(
context,
),
icon: action.getIconData(),
);
}).toList();
}

void _handleStateContextMenu() {
openingPopupMenu.toggle();
}

void _handleContextMenuAction({
required BuildContext context,
required TapDownDetails tapDownDetails,
required String number,
}) async {
final offset = tapDownDetails.globalPosition;
final listActions = _mapPopupMenuActionsToContextMenuActions(
context: context,
actions: phoneNumberContextMenuOnWeb,
);
final selectedActionIndex = await showTwakeContextMenu(
context: context,
offset: offset,
listActions: listActions,
onClose: _handleStateContextMenu,
);
if (selectedActionIndex != null && selectedActionIndex is int) {
_handleClickOnContextMenuItem(
action: phoneNumberContextMenuOnWeb[selectedActionIndex],
number: number,
);
}
}

Future<dynamic> showTwakeContextMenu({
required List<ContextMenuAction> listActions,
required Offset offset,
required BuildContext context,
double? verticalPadding,
VoidCallback? onClose,
}) async {
dynamic result;
await showDialog(
context: context,
barrierColor: Colors.transparent,
barrierDismissible: false,
builder: (dialogContext) => TwakeContextMenu(
dialogContext: dialogContext,
listActions: listActions,
position: offset,
verticalPadding: verticalPadding,
),
).then((value) {
result = value;
onClose?.call();
});
return result;
}

void _handleClickOnContextMenuItem({
required PhoneNumberContextMenuActions action,
required String number,
}) async {
switch (action) {
case PhoneNumberContextMenuActions.copy:
Logs().i('LinkifyMixin: handleContextMenuAction: copyNumber $number');
await Clipboard.setData(ClipboardData(text: number));
break;
default:
break;
}
}

Future<void> _handleShowPullDownMenu({
required BuildContext context,
required TapDownDetails tapDownDetails,
required String number,
}) {
return showPullDownMenu(
context: context,
items: [
PullDownMenuItem(
onTap: () async {
final phoneUri = Uri(
scheme: "tel",
path: number.replaceAll(' ', ''),
);
if (await canLaunchUrl(phoneUri)) {
launchUrl(phoneUri);
} else {
Logs().e(
'LinkifyMixin: handleOnTappedLink: Cannot launch phoneUri: $phoneUri',
);
}
},
title: L10n.of(context)!.callViaCarrier,
icon: Icons.call_outlined,
),
PullDownMenuItem(
title: L10n.of(context)!.copyNumber,
onTap: () async {
await Clipboard.setData(
ClipboardData(text: number),
);
},
icon: Icons.content_copy_outlined,
),
const PullDownMenuDivider.large(),
PullDownMenuItem(
title: number,
onTap: () {},
itemTheme: PullDownMenuItemTheme(
textStyle: context.textTheme.bodyLarge!.copyWith(
color: LinagoraRefColors.material().neutral[30],
),
),
),
],
position: tapDownDetails.globalPosition & Size.zero,
);
}

void handleOnTappedLinkHtml({
required BuildContext context,
required TapDownDetails details,
required Link link,
}) async {
Logs().i(
'LinkifyMixin: handleOnTappedLink: type: ${link.type} link: ${link.value}',
);
switch (link.type) {
case LinkType.url:
UrlLauncher(context, url: link.value.toString()).launchUrl();
break;
case LinkType.phone:
if (PlatformInfos.isMobile) {
await _handleShowPullDownMenu(
context: context,
tapDownDetails: details,
number: link.value.toString(),
);
} else {
_handleContextMenuAction(
context: context,
tapDownDetails: details,
number: link.value.toString(),
);
}
break;
default:
Logs().i('LinkifyMixin: handleOnTappedLink: Unhandled link: $link');
break;
}
}

void dispose() {
openingPopupMenu.dispose();
}
}
2 changes: 1 addition & 1 deletion lib/utils/url_launcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class UrlLauncher with GoToDraftChatMixin {
return;
}
}
launchUrlString(url!);
launchUrlString('https://$url');
tddang-linagora marked this conversation as resolved.
Show resolved Hide resolved
return;
}
if (uri.host.isEmpty) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import 'package:fluffychat/domain/app_state/preview_url/get_preview_url_success.dart';
import 'package:fluffychat/pages/chat/events/formatted_text_widget.dart';
import 'package:fluffychat/presentation/extensions/media/url_preview_extension.dart';
import 'package:fluffychat/presentation/mixins/linkify_mixin.dart';
import 'package:fluffychat/utils/string_extension.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/mixins/get_preview_url_mixin.dart';
import 'package:fluffychat/widgets/twake_components/twake_preview_link/twake_link_preview_item.dart';
import 'package:fluffychat/widgets/twake_components/twake_preview_link/twake_link_preview_item_style.dart';
import 'package:fluffychat/widgets/twake_components/twake_preview_link/twake_link_view.dart';
import 'package:flutter/material.dart';
import 'package:linagora_design_flutter/linagora_design_flutter.dart';
import 'package:linkfy_text/linkfy_text.dart';
import 'package:matrix/matrix.dart' hide Visibility;
import 'package:matrix_link_text/link_text.dart';
import 'package:skeletonizer/skeletonizer.dart';

class TwakeLinkPreview extends StatefulWidget {
Expand Down Expand Up @@ -38,7 +38,7 @@ class TwakeLinkPreview extends StatefulWidget {
}

class TwakeLinkPreviewController extends State<TwakeLinkPreview>
with GetPreviewUrlMixin, AutomaticKeepAliveClientMixin {
with GetPreviewUrlMixin, AutomaticKeepAliveClientMixin, LinkifyMixin {
String? get firstValidUrl => widget.localizedBody.getFirstValidUrl();

Uri get uri => Uri.parse(firstValidUrl ?? '');
Expand Down Expand Up @@ -70,13 +70,20 @@ class TwakeLinkPreviewController extends State<TwakeLinkPreview>
linkStyle: widget.linkStyle,
fontSize: widget.fontSize,
)
: LinkText(
text: widget.localizedBody,
: MatrixLinkifyText(
widget.localizedBody,
textStyle: widget.richTextStyle,
linkStyle: widget.linkStyle,
linkTypes: const [
LinkType.url,
LinkType.phone,
],
textAlign: TextAlign.start,
onLinkTap: (url) =>
UrlLauncher(context, url: url.toString()).launchUrl(),
onTapDownLink: (tapDownDetails, link) => handleOnTappedLinkHtml(
context: context,
details: tapDownDetails,
link: link,
),
),
previewItemWidget: ValueListenableBuilder(
valueListenable: getPreviewUrlStateNotifier,
Expand Down
Loading