diff --git a/lib/view/dialog/user_select_dialog.dart b/lib/view/dialog/user_select_dialog.dart index e0e0b0bb..cdfcb81a 100644 --- a/lib/view/dialog/user_select_dialog.dart +++ b/lib/view/dialog/user_select_dialog.dart @@ -10,6 +10,7 @@ import '../../model/account.dart'; import '../../provider/api/i_notifier_provider.dart'; import '../../provider/api/search_users_by_username_provider.dart'; import '../../provider/recently_used_users_notifier_provider.dart'; +import '../../util/punycode.dart'; import '../widget/error_message.dart'; import '../widget/user_preview.dart'; import '../widget/user_sheet.dart'; @@ -51,8 +52,8 @@ class UserSelectDialog extends HookConsumerWidget { ? ref.watch( searchUsersByUsernameProvider( account, - username.value, - host.value, + username.value.isNotEmpty ? username.value : null, + host.value.isNotEmpty ? toAscii(host.value) : null, ), ) : null; diff --git a/lib/view/widget/mfm_keyboard.dart b/lib/view/widget/mfm_keyboard.dart index de6c7e6a..2d4085d2 100644 --- a/lib/view/widget/mfm_keyboard.dart +++ b/lib/view/widget/mfm_keyboard.dart @@ -20,6 +20,7 @@ import '../../provider/recently_used_users_notifier_provider.dart'; import '../../provider/search_custom_emojis_provider.dart'; import '../../provider/search_unicode_emojis_provider.dart'; import '../../util/pick_date_time.dart'; +import '../../util/punycode.dart'; import '../dialog/user_select_dialog.dart'; import 'emoji_picker.dart'; import 'emoji_widget.dart'; @@ -250,7 +251,8 @@ class MfmEmojiKeyboard extends HookConsumerWidget { final selectionIndex = max(0, controller.selection.start); final textBeforeSelection = controller.text.substring(0, selectionIndex); - final match = RegExp(r':?(\S*)$').firstMatch(textBeforeSelection); + final match = RegExp(r':(\S*)$').firstMatch(textBeforeSelection) ?? + RegExp(r'(\S*)$').firstMatch(textBeforeSelection); query.value = match?[1] ?? ''; final tagIndex = selectionIndex - (match?[0]?.length ?? 0) + 1; isAfterCloseTag.value = @@ -413,9 +415,9 @@ class MfmFnKeyboard extends HookConsumerWidget { final selectionIndex = max(0, controller.selection.start); final textBeforeSelection = controller.text.substring(0, selectionIndex); - query.value = - RegExp(r'(\$\[)?(\S*)$').firstMatch(textBeforeSelection)?[2] ?? - ''; + final match = RegExp(r'\$\[(\S*)$').firstMatch(textBeforeSelection) ?? + RegExp(r'(\S*)$').firstMatch(textBeforeSelection); + query.value = match?[1] ?? ''; periodIndex.value = query.value.indexOf('.'); } @@ -583,17 +585,23 @@ class MfmMentionKeyboard extends HookConsumerWidget { final selectionIndex = max(0, controller.selection.start); final textBeforeSelection = controller.text.substring(0, selectionIndex); - query.value = - RegExp(r'@?(\S*)$').firstMatch(textBeforeSelection)?[1] ?? ''; - if (query.value case final query when query.isNotEmpty) { - final acct = query.split('@'); + final match = RegExp(r'(@([a-zA-Z0-9_.-]+))?@([^@\s]*)$') + .firstMatch(textBeforeSelection); + query.value = match?[0]?.substring(1) ?? ''; + final first = match?[2]; + final second = match?[3]; + final username = first ?? second; + final host = first != null && second != null ? toAscii(second) : null; + if (username != null) { users.value = await ref.read( searchUsersByUsernameProvider( account, - acct.first, - acct.elementAtOrNull(1), + username, + host, ).future, ); + } else { + users.value = []; } } @@ -617,8 +625,9 @@ class MfmMentionKeyboard extends HookConsumerWidget { account: account, username: user.username, host: user.host ?? account.host, - onTap: () => controller.insert( - '${user.acct.substring(query.value.length + 1)} ', + onTap: () => controller.replace( + query.value.length + 1, + '${user.acct} ', ), ), ), @@ -653,8 +662,9 @@ class MfmHashtagKeyboard extends HookConsumerWidget { final selectionIndex = max(0, controller.selection.start); final textBeforeSelection = controller.text.substring(0, selectionIndex); - query.value = - RegExp(r'#?(\S*)$').firstMatch(textBeforeSelection)?[1] ?? ''; + final match = RegExp(r'#(\S*)$').firstMatch(textBeforeSelection) ?? + RegExp(r'(\S*)$').firstMatch(textBeforeSelection); + query.value = match?[1] ?? ''; if (query.value case final query when query.isNotEmpty) { hashtags.value = await ref.read(searchHashtagsProvider(account, query).future); diff --git a/test/view/widget/mfm_keyboard_test.dart b/test/view/widget/mfm_keyboard_test.dart index 3988acc4..f722574f 100644 --- a/test/view/widget/mfm_keyboard_test.dart +++ b/test/view/widget/mfm_keyboard_test.dart @@ -37,11 +37,10 @@ Future setupWidget( void main() { group('getLastTag', () { - final controller = TextEditingController(); - final widget = - MfmKeyboard(account: Account.dummy(), controller: controller); - test('emoji', () { + final controller = TextEditingController(); + final widget = + MfmKeyboard(account: Account.dummy(), controller: controller); controller.text = ':'; controller.selection = const TextSelection.collapsed(offset: 0); expect(widget.getLastTag(), equals((null, -1))); @@ -58,6 +57,9 @@ void main() { }); test('mfmFn', () { + final controller = TextEditingController(); + final widget = + MfmKeyboard(account: Account.dummy(), controller: controller); controller.text = r'$['; controller.selection = const TextSelection.collapsed(offset: 0); expect(widget.getLastTag(), equals((null, -1))); @@ -74,6 +76,9 @@ void main() { }); test('mention', () { + final controller = TextEditingController(); + final widget = + MfmKeyboard(account: Account.dummy(), controller: controller); controller.text = '@'; controller.selection = const TextSelection.collapsed(offset: 0); expect(widget.getLastTag(), equals((null, -1))); @@ -87,6 +92,9 @@ void main() { }); test('hashtag', () { + final controller = TextEditingController(); + final widget = + MfmKeyboard(account: Account.dummy(), controller: controller); controller.text = '#'; controller.selection = const TextSelection.collapsed(offset: 0); expect(widget.getLastTag(), equals((null, -1))); @@ -100,6 +108,9 @@ void main() { }); test('mixed', () { + final controller = TextEditingController(); + final widget = + MfmKeyboard(account: Account.dummy(), controller: controller); controller.text = r':emoji: #hashtag $[tada a] @user'; controller.selection = const TextSelection.collapsed(offset: 0); expect(widget.getLastTag(), equals((null, -1))); @@ -166,6 +177,26 @@ void main() { expect(controller.text, '❤️'); }); + testWidgets( + 'should show an emoji keyboard if an open tag is between characters', + (tester) async { + const account = Account(host: 'misskey.tld', username: 'testuser'); + final controller = TextEditingController(text: 'a:b'); + controller.selection = const TextSelection.collapsed(offset: 2); + final container = await setupWidget( + tester, + account: account, + controller: controller, + ); + await container + .read(accountSettingsNotifierProvider(account).notifier) + .setRecentlyUsedEmojis(['❤️']); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const ValueKey('❤️'))); + expect(controller.text, 'a❤️b'); + }, + ); + testWidgets('should not show an emoji keyboard after a close tag', (tester) async { const account = Account(host: 'misskey.tld', username: 'testuser'); @@ -252,6 +283,23 @@ void main() { expect(controller.text, r'$[spin.speed=1.5s,x'); }); + testWidgets( + 'should show an MFM fn keyboard if an open tag is between characters', + (tester) async { + const account = Account(host: 'misskey.tld', username: 'testuser'); + final controller = TextEditingController(text: r'a$[b'); + controller.selection = const TextSelection.collapsed(offset: 3); + await setupWidget( + tester, + account: account, + controller: controller, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('tada')); + expect(controller.text, r'a$[tada b'); + }, + ); + testWidgets('should not show an MFM fn keyboard after a close tag', (tester) async { const account = Account(host: 'misskey.tld', username: 'testuser'); @@ -349,7 +397,27 @@ void main() { expect(controller.text, '@testuser@misskey2.tld '); }); - testWidgets('should not show an mention keyboard after a space', + testWidgets( + 'should show a mention keyboard if an open tag is between characters', + (tester) async { + const account = Account(host: 'misskey.tld', username: 'testuser'); + final controller = TextEditingController(text: 'a:b'); + controller.selection = const TextSelection.collapsed(offset: 2); + final container = await setupWidget( + tester, + account: account, + controller: controller, + ); + await container + .read(accountSettingsNotifierProvider(account).notifier) + .setRecentlyUsedEmojis(['❤️']); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const ValueKey('❤️'))); + expect(controller.text, 'a❤️b'); + }, + ); + + testWidgets('should not show a mention keyboard after a space', (tester) async { const account = Account(host: 'misskey.tld', username: 'testuser'); final controller = TextEditingController();