diff --git a/.changes/1268-clickable-room-id.md b/.changes/1268-clickable-room-id.md new file mode 100644 index 000000000000..1960c49739fa --- /dev/null +++ b/.changes/1268-clickable-room-id.md @@ -0,0 +1,2 @@ +- [enhance] Copy room link instead of roomId +- [fix] Click on room link will perform appropriate action \ No newline at end of file diff --git a/app/lib/common/utils/rooms.dart b/app/lib/common/utils/rooms.dart index 25ea07495125..f55aa2566011 100644 --- a/app/lib/common/utils/rooms.dart +++ b/app/lib/common/utils/rooms.dart @@ -42,6 +42,7 @@ Future joinRoom( if (!context.mounted) { return; } + Navigator.of(context, rootNavigator: true).pop(); showAdaptiveDialog( barrierDismissible: false, context: context, diff --git a/app/lib/features/chat/chat_utils/chat_utils.dart b/app/lib/features/chat/chat_utils/chat_utils.dart index be37b17a8e10..4e7751cd5604 100644 --- a/app/lib/features/chat/chat_utils/chat_utils.dart +++ b/app/lib/features/chat/chat_utils/chat_utils.dart @@ -1,4 +1,10 @@ -import 'package:html/dom.dart'; +import 'package:acter/common/providers/room_providers.dart'; +import 'package:acter/common/utils/rooms.dart'; +import 'package:acter/common/utils/routes.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:html/dom.dart' as html; class UserMentionMessageData { String parsedMessage; @@ -14,7 +20,7 @@ class UserMentionMessageData { UserMentionMessageData parseUserMentionMessage( String message, - Element aTagElement, + html.Element aTagElement, ) { String msg = message; String userName = ''; @@ -51,3 +57,112 @@ UserMentionMessageData parseUserMentionMessage( displayName: displayName, ); } + +String? getRoomIdFromLink(Uri uri) { + final link = Uri.decodeFull(uri.toString()); + + // Match regex for matrix room link + final urlRegexp = RegExp( + r'https://matrix\.to/#/(?.+):(?.+)+', + caseSensitive: false, + ); + final matches = urlRegexp.firstMatch(link); + + //Link is type of matrix room link + if (matches != null) { + final roomId = matches.namedGroup('roomId'); + var server = matches.namedGroup('server'); + + //Check & remove if string contains "?via= pattern" + server = server!.split('?via=').first; + + //Create complete roomId with home server information + var roomIdWithServer = '$roomId:$server'; + + //Return roomId + return roomIdWithServer; + } + + //Link is other than matrix room link + return null; +} + +Future navigateToRoomOrAskToJoin( + BuildContext context, + WidgetRef ref, + String roomId, +) async { + ///Get room from roomId + final room = await ref.watch(maybeRoomProvider(roomId).future); + if (!context.mounted) return; + + /// Navigate to Room is already joined + if (room != null && room.isJoined()) { + //Navigate to Space + if (room.isSpace()) { + context.pushNamed( + Routes.space.name, + pathParameters: {'spaceId': room.roomIdStr()}, + ); + } + //Navigate to Chat + else { + context.goNamed( + Routes.chatroom.name, + pathParameters: {'roomId': room.roomIdStr()}, + ); + } + } + + /// Ask to join room if not yet joined + else { + askToJoinRoom(context, ref, roomId); + } +} + +void askToJoinRoom( + BuildContext context, + WidgetRef ref, + String roomId, +) async { + showModalBottomSheet( + context: context, + isDismissible: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(20), + topLeft: Radius.circular(20), + ), + ), + builder: (ctx) => Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'You are not part of this group. Would you like to join?', + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () async { + Navigator.of(context).pop(); + final server = roomId.split(':').last; + await joinRoom( + context, + ref, + 'Trying to join $roomId', + roomId, + server, + (roomId) => navigateToRoomOrAskToJoin(context, ref, roomId), + ); + }, + child: const Text('Join Room'), + ), + ], + ), + ), + ); +} diff --git a/app/lib/features/chat/pages/room_profile_page.dart b/app/lib/features/chat/pages/room_profile_page.dart index d208a3235e9b..d2a6ff7a45ee 100644 --- a/app/lib/features/chat/pages/room_profile_page.dart +++ b/app/lib/features/chat/pages/room_profile_page.dart @@ -139,9 +139,11 @@ class RoomProfilePage extends ConsumerWidget { tiles: [ SettingsTile( onPressed: (ctx) { + //FIXME : ?via=$serverName data should be handle from rust helper function + final serverName = roomId.split(':').last; Clipboard.setData( ClipboardData( - text: roomId, + text: 'https://matrix.to/#/$roomId?via=$serverName', ), ); customMsgSnackbar( @@ -150,7 +152,7 @@ class RoomProfilePage extends ConsumerWidget { ); }, title: Text( - 'Copy Room ID', + 'Copy room link', style: tileTextTheme, ), leading: const Icon(Atlas.chain_link_thin, size: 18), diff --git a/app/lib/features/chat/widgets/text_message_builder.dart b/app/lib/features/chat/widgets/text_message_builder.dart index 0fe421dc46d2..969fa344ef3a 100644 --- a/app/lib/features/chat/widgets/text_message_builder.dart +++ b/app/lib/features/chat/widgets/text_message_builder.dart @@ -1,6 +1,7 @@ import 'package:acter/common/themes/app_theme.dart'; import 'package:acter/common/themes/chat_theme.dart'; import 'package:acter/common/utils/utils.dart'; +import 'package:acter/features/chat/chat_utils/chat_utils.dart'; import 'package:acter/features/chat/providers/chat_providers.dart'; import 'package:acter/features/home/providers/client_providers.dart'; import 'package:acter_flutter_sdk/acter_flutter_sdk_ffi.dart'; @@ -56,7 +57,7 @@ class _TextMessageBuilderConsumerState //remove mx-reply tags. String parsedString = simplifyBody(widget.message.text); final urlRegexp = RegExp( - r'https://matrix\.to/#/@[A-Za-z0-9\-]+:[A-Za-z0-9\-]+\.[A-Za-z0-9\-]+', + r'https://matrix\.to/#/[@!#][A-Za-z0-9\-]+:[A-Za-z0-9\-]+\.[A-Za-z0-9\-]+', caseSensitive: false, ); final matches = urlRegexp.allMatches(parsedString); @@ -153,9 +154,7 @@ class _TextWidget extends ConsumerWidget { maxLines: isReply ? 3 : null, ) : Html( - onLinkTap: (url) async { - await openLink(url.toString(), context); - }, + onLinkTap: (url) => onLinkTap(url, context, ref), backgroundColor: Colors.transparent, data: message.text, shrinkToFit: true, @@ -184,4 +183,19 @@ class _TextWidget extends ConsumerWidget { ], ); } + + Future onLinkTap(Uri uri, BuildContext context, WidgetRef ref) async { + final roomId = getRoomIdFromLink(uri); + + ///If link is type of matrix room link + if (roomId != null) { + await navigateToRoomOrAskToJoin(context, ref, roomId); + } + + ///If link is other than matrix room link + ///Then open it on browser + else { + await openLink(uri.toString(), context); + } + } } diff --git a/app/test/features/chat/chat_message_test.dart b/app/test/features/chat/chat_message_test.dart index 0bc3b7330d30..d4634f16d364 100644 --- a/app/test/features/chat/chat_message_test.dart +++ b/app/test/features/chat/chat_message_test.dart @@ -10,151 +10,239 @@ void main() { document = Document(); }); - group('User mention chat message parsing test -', () { - test('Parse massage with one instance of user mention', () { - String msg = - '

hello acter1

'; - - Element aTagElement = document.createElement('a'); - aTagElement.innerHtml = 'acter1'; - final attributes = { - 'href': 'https://matrix.to/#/@acter1:matrix.org', - }; - aTagElement.attributes.addAll(attributes); - - final resultData = parseUserMentionMessage(msg, aTagElement); - - final messageDocument = parse(resultData.parsedMessage); - final messageBodyText = messageDocument.body?.text ?? ''; - expect(messageBodyText, 'hello @acter1'); - }); - - test('Parse massage with multiple instances of different user mentions', () { - String msg = - '

hello acter2 acter1

'; - - Element aTagElement1 = document.createElement('a'); - aTagElement1.innerHtml = 'acter2'; - final attributes1 = { - 'href': 'https://matrix.to/#/@acter2:matrix.org', - }; - aTagElement1.attributes.addAll(attributes1); - - Element aTagElement2 = document.createElement('a'); - aTagElement2.innerHtml = 'acter1'; - final attributes2 = { - 'href': 'https://matrix.to/#/@acter1:matrix.org', - }; - aTagElement2.attributes.addAll(attributes2); - - document.append(aTagElement1); - document.append(aTagElement2); - - final aTagElementList = document.getElementsByTagName('a'); - for (final aTagElement in aTagElementList) { - final resultData = parseUserMentionMessage(msg, aTagElement); - msg = resultData.parsedMessage; - } - - final messageDocument = parse(msg); - final messageBodyText = messageDocument.body?.text ?? ''; - expect(messageBodyText, 'hello @acter2 @acter1'); - }); - - test('Parse massage with duplicate instances of user mentions', () { - String msg = - '

hello acter2 acter2

'; - - Element aTagElement1 = document.createElement('a'); - aTagElement1.innerHtml = 'acter2'; - final attributes1 = { - 'href': 'https://matrix.to/#/@acter2:matrix.org', - }; - aTagElement1.attributes.addAll(attributes1); - - document.append(aTagElement1); - document.append(aTagElement1); - - final aTagElementList = document.getElementsByTagName('a'); - for (final aTagElement in aTagElementList) { - final resultData = parseUserMentionMessage(msg, aTagElement); - msg = resultData.parsedMessage; - } - - final messageDocument = parse(msg); - final messageBodyText = messageDocument.body?.text ?? ''; - expect(messageBodyText, 'hello @acter2 @acter2'); - }); - - test( + group( + 'User mention chat message parsing test -', + () { + test( + 'Parse massage with one instance of user mention', + () { + String msg = + '

hello acter1

'; + + Element aTagElement = document.createElement('a'); + aTagElement.innerHtml = 'acter1'; + final attributes = { + 'href': 'https://matrix.to/#/@acter1:matrix.org', + }; + aTagElement.attributes.addAll(attributes); + + final resultData = parseUserMentionMessage(msg, aTagElement); + + final messageDocument = parse(resultData.parsedMessage); + final messageBodyText = messageDocument.body?.text ?? ''; + expect(messageBodyText, 'hello @acter1'); + }, + ); + + test( + 'Parse massage with multiple instances of different user mentions', + () { + String msg = + '

hello acter2 acter1

'; + + Element aTagElement1 = document.createElement('a'); + aTagElement1.innerHtml = 'acter2'; + final attributes1 = { + 'href': 'https://matrix.to/#/@acter2:matrix.org', + }; + aTagElement1.attributes.addAll(attributes1); + + Element aTagElement2 = document.createElement('a'); + aTagElement2.innerHtml = 'acter1'; + final attributes2 = { + 'href': 'https://matrix.to/#/@acter1:matrix.org', + }; + aTagElement2.attributes.addAll(attributes2); + + document.append(aTagElement1); + document.append(aTagElement2); + + final aTagElementList = document.getElementsByTagName('a'); + for (final aTagElement in aTagElementList) { + final resultData = parseUserMentionMessage(msg, aTagElement); + msg = resultData.parsedMessage; + } + + final messageDocument = parse(msg); + final messageBodyText = messageDocument.body?.text ?? ''; + expect(messageBodyText, 'hello @acter2 @acter1'); + }, + ); + + test( + 'Parse massage with duplicate instances of user mentions', + () { + String msg = + '

hello acter2 acter2

'; + + Element aTagElement1 = document.createElement('a'); + aTagElement1.innerHtml = 'acter2'; + final attributes1 = { + 'href': 'https://matrix.to/#/@acter2:matrix.org', + }; + aTagElement1.attributes.addAll(attributes1); + + document.append(aTagElement1); + document.append(aTagElement1); + + final aTagElementList = document.getElementsByTagName('a'); + for (final aTagElement in aTagElementList) { + final resultData = parseUserMentionMessage(msg, aTagElement); + msg = resultData.parsedMessage; + } + + final messageDocument = parse(msg); + final messageBodyText = messageDocument.body?.text ?? ''; + expect(messageBodyText, 'hello @acter2 @acter2'); + }, + ); + + test( 'Parse massage with multiple instances of user mentions and one duplicate mention', () { - String msg = - '

hello acter2 acter1 acter2

'; - - Element aTagElement1 = document.createElement('a'); - aTagElement1.innerHtml = 'acter2'; - final attributes1 = { - 'href': 'https://matrix.to/#/@acter2:matrix.org', - }; - aTagElement1.attributes.addAll(attributes1); - - Element aTagElement2 = document.createElement('a'); - aTagElement2.innerHtml = 'acter1'; - final attributes2 = { - 'href': 'https://matrix.to/#/@acter1:matrix.org', - }; - aTagElement2.attributes.addAll(attributes2); - - document.append(aTagElement1); - document.append(aTagElement2); - document.append(aTagElement1); - - final aTagElementList = document.getElementsByTagName('a'); - for (final aTagElement in aTagElementList) { - final resultData = parseUserMentionMessage(msg, aTagElement); - msg = resultData.parsedMessage; - } - - final messageDocument = parse(msg); - final messageBodyText = messageDocument.body?.text ?? ''; - expect(messageBodyText, 'hello @acter2 @acter1 @acter2'); - }); - - test( + String msg = + '

hello acter2 acter1 acter2

'; + + Element aTagElement1 = document.createElement('a'); + aTagElement1.innerHtml = 'acter2'; + final attributes1 = { + 'href': 'https://matrix.to/#/@acter2:matrix.org', + }; + aTagElement1.attributes.addAll(attributes1); + + Element aTagElement2 = document.createElement('a'); + aTagElement2.innerHtml = 'acter1'; + final attributes2 = { + 'href': 'https://matrix.to/#/@acter1:matrix.org', + }; + aTagElement2.attributes.addAll(attributes2); + + document.append(aTagElement1); + document.append(aTagElement2); + document.append(aTagElement1); + + final aTagElementList = document.getElementsByTagName('a'); + for (final aTagElement in aTagElementList) { + final resultData = parseUserMentionMessage(msg, aTagElement); + msg = resultData.parsedMessage; + } + + final messageDocument = parse(msg); + final messageBodyText = messageDocument.body?.text ?? ''; + expect(messageBodyText, 'hello @acter2 @acter1 @acter2'); + }, + ); + + test( 'Parse massage with multiple instances of user mentions and multiple duplicate mentions', () { - String msg = - '

hello acter2 acter1 acter2 acter1

'; - - Element aTagElement1 = document.createElement('a'); - aTagElement1.innerHtml = 'acter2'; - final attributes1 = { - 'href': 'https://matrix.to/#/@acter2:matrix.org', - }; - aTagElement1.attributes.addAll(attributes1); - - Element aTagElement2 = document.createElement('a'); - aTagElement2.innerHtml = 'acter1'; - final attributes2 = { - 'href': 'https://matrix.to/#/@acter1:matrix.org', - }; - aTagElement2.attributes.addAll(attributes2); - - document.append(aTagElement1); - document.append(aTagElement2); - document.append(aTagElement1); - document.append(aTagElement2); - - final aTagElementList = document.getElementsByTagName('a'); - for (final aTagElement in aTagElementList) { - final resultData = parseUserMentionMessage(msg, aTagElement); - msg = resultData.parsedMessage; - } - - final messageDocument = parse(msg); - final messageBodyText = messageDocument.body?.text ?? ''; - expect(messageBodyText, 'hello @acter2 @acter1 @acter2 @acter1'); - }); - }); + String msg = + '

hello acter2 acter1 acter2 acter1

'; + + Element aTagElement1 = document.createElement('a'); + aTagElement1.innerHtml = 'acter2'; + final attributes1 = { + 'href': 'https://matrix.to/#/@acter2:matrix.org', + }; + aTagElement1.attributes.addAll(attributes1); + + Element aTagElement2 = document.createElement('a'); + aTagElement2.innerHtml = 'acter1'; + final attributes2 = { + 'href': 'https://matrix.to/#/@acter1:matrix.org', + }; + aTagElement2.attributes.addAll(attributes2); + + document.append(aTagElement1); + document.append(aTagElement2); + document.append(aTagElement1); + document.append(aTagElement2); + + final aTagElementList = document.getElementsByTagName('a'); + for (final aTagElement in aTagElementList) { + final resultData = parseUserMentionMessage(msg, aTagElement); + msg = resultData.parsedMessage; + } + + final messageDocument = parse(msg); + final messageBodyText = messageDocument.body?.text ?? ''; + expect(messageBodyText, 'hello @acter2 @acter1 @acter2 @acter1'); + }, + ); + }, + ); + + group( + 'Matrix room link parsing test -', + () { + test( + 'Parse private room link - With "Via home sever" details', + () { + //Static + String roodIdLink = + 'https://matrix.to/#/!zxDsxBeywEsePmARgd:m-1.acter.global?via=m-1.acter.global'; + + final resultData = getRoomIdFromLink(Uri.parse(roodIdLink)); + + expect(resultData, '!zxDsxBeywEsePmARgd:m-1.acter.global'); + }, + ); + + test( + 'Parse private room link - Without "Via home sever" details', + () { + String roodIdLink = + 'https://matrix.to/#/!zxDsxBeywEsePmARgd:m-1.acter.global'; + + final resultData = getRoomIdFromLink(Uri.parse(roodIdLink)); + + expect(resultData, '!zxDsxBeywEsePmARgd:m-1.acter.global'); + }, + ); + + test( + 'Parse public room link - With "#" sign at the starting of link', + () { + String roodIdLink = 'https://matrix.to/#/#acterroom2:matrix.org'; + + final resultData = getRoomIdFromLink(Uri.parse(roodIdLink)); + + expect(resultData, '#acterroom2:matrix.org'); + }, + ); + + test( + 'Parse public room link - With "%23" sign at the starting of link', + () { + String roodIdLink = 'https://matrix.to/#/%23acterroom123:matrix.org'; + + final resultData = getRoomIdFromLink(Uri.parse(roodIdLink)); + + expect(resultData, '#acterroom123:matrix.org'); + }, + ); + + test( + 'Parse invalid room link', + () { + String roodIdLink = 'https://matrix.to/#/#acterroom2'; + + final resultData = getRoomIdFromLink(Uri.parse(roodIdLink)); + + expect(resultData, null); + }, + ); + + test( + 'Parse link which is other than room link', + () { + String roodIdLink = 'https://www.google.com'; + + final resultData = getRoomIdFromLink(Uri.parse(roodIdLink)); + + expect(resultData, null); + }, + ); + }, + ); }