fix: route user-tag taps to the group member screen for members (#686)

This commit is contained in:
Danny M
2026-05-21 11:06:20 +02:00
committed by GitHub
parent 82a0dc1b33
commit cebf3a1239
6 changed files with 96 additions and 2 deletions
+1
View File
@@ -547,6 +547,7 @@ class ChatScreen extends HookConsumerWidget {
: null,
onHorizontalDragEnd: isSearchMode ? null : () => input.setReplyingTo(message),
mentionDisplayName: resolveMentionDisplayName,
groupId: isGroupChat ? groupId : null,
onRetry:
!isSearchMode && isOwnMessage && message.deliveryStatus is DeliveryStatus_Failed
? () async {
+16
View File
@@ -12,6 +12,7 @@ import 'package:whitenoise/hooks/use_user_metadata.dart';
import 'package:whitenoise/l10n/l10n.dart';
import 'package:whitenoise/providers/account_pubkey_provider.dart';
import 'package:whitenoise/routes.dart';
import 'package:whitenoise/screens/start_chat_screen.dart';
import 'package:whitenoise/theme.dart';
import 'package:whitenoise/utils/avatar_color.dart';
import 'package:whitenoise/utils/metadata.dart' show presentName;
@@ -87,6 +88,21 @@ class GroupMemberScreen extends HookConsumerWidget {
return null;
}, [membersState.error]);
final didRedirect = useRef(false);
useEffect(() {
if (didRedirect.value) return null;
if (membersState.isLoading || membersState.error != null) return null;
if (membersState.members.contains(memberPubkey)) return null;
didRedirect.value = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!context.mounted) return;
Routes.goBack(context);
if (!context.mounted) return;
StartChatScreen.show(context, userPubkey: memberPubkey);
});
return null;
}, [membersState.isLoading, membersState.members]);
final isCurrentUserAdmin = membersState.admins.contains(accountPubkey);
final isMemberAdmin = membersState.admins.contains(memberPubkey);
final displayName = presentName(metadata) ?? memberPubkey.substring(0, 8);
+1 -1
View File
@@ -191,8 +191,8 @@ class StartChatScreen extends HookConsumerWidget {
child: SafeArea(
child: Column(
children: [
const Spacer(),
WnSlate(
shrinkWrapContent: true,
header: WnSlateNavigationHeader(
title: context.l10n.startNewChat,
onNavigate: () => Routes.goBack(context),
+8 -1
View File
@@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:whitenoise/hooks/use_chat_messages.dart' show ChatMessageQuoteData;
import 'package:whitenoise/l10n/l10n.dart';
import 'package:whitenoise/routes.dart';
import 'package:whitenoise/screens/start_chat_screen.dart';
import 'package:whitenoise/src/rust/api/markdown.dart';
import 'package:whitenoise/src/rust/api/messages.dart';
@@ -38,6 +39,7 @@ class ChatMessageBubble extends StatelessWidget {
final VoidCallback? onHorizontalDragEnd;
final VoidCallback? onRetry;
final String? Function(String hexPubkey)? mentionDisplayName;
final String? groupId;
const ChatMessageBubble({
super.key,
@@ -60,6 +62,7 @@ class ChatMessageBubble extends StatelessWidget {
this.onHorizontalDragEnd,
this.onRetry,
this.mentionDisplayName,
this.groupId,
});
ChatStatusType? get _deliveryStatusType {
@@ -162,7 +165,11 @@ class ChatMessageBubble extends StatelessWidget {
onNostrTap: (hrp, bech32) async {
if (hrp == MarkdownNostrHrp.npub) {
final hex = hexFromNpub(bech32);
if (hex != null && context.mounted) {
if (hex == null || !context.mounted) return;
final gid = groupId;
if (gid != null) {
Routes.pushToGroupMember(context, gid, hex);
} else {
await StartChatScreen.show(context, userPubkey: hex);
}
return;
@@ -5,6 +5,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart' show AsyncData;
import 'package:flutter_test/flutter_test.dart';
import 'package:whitenoise/providers/auth_provider.dart';
import 'package:whitenoise/routes.dart';
import 'package:whitenoise/screens/group_member_screen.dart';
import 'package:whitenoise/screens/start_chat_screen.dart';
import 'package:whitenoise/src/rust/api/groups.dart';
import 'package:whitenoise/src/rust/api/metadata.dart';
import 'package:whitenoise/src/rust/frb_generated.dart';
@@ -726,5 +728,27 @@ void main() {
expect(find.text('Add to group'), findsWidgets);
});
});
group('non-member redirect', () {
testWidgets('redirects to StartChatScreen when the user is not in the group', (
tester,
) async {
_api.adminsList = [_testPubkey];
await pumpGroupMemberScreen(tester, memberPubkey: testPubkeyD);
await tester.pumpAndSettle();
expect(find.byType(GroupMemberScreen), findsNothing);
expect(find.byType(StartChatScreen), findsOneWidget);
});
testWidgets('does not redirect when the user is a member', (tester) async {
_api.adminsList = [_testPubkey];
await pumpGroupMemberScreen(tester, memberPubkey: _memberPubkey);
await tester.pumpAndSettle();
expect(find.byType(GroupMemberScreen), findsOneWidget);
expect(find.byType(StartChatScreen), findsNothing);
});
});
});
}
@@ -919,6 +919,42 @@ void main() {
);
});
testWidgets('npub mention in a group navigates to the group member screen', (tester) async {
await _mountBubbleWithRouter(
tester,
ChatMessageBubble(
message: withDoc(
const MarkdownDocument(
blocks: [
MarkdownBlock.paragraph(
inlines: [
MarkdownInline.nostrMention(
entity: MarkdownNostrEntity(
hrp: MarkdownNostrHrp.npub,
bech32: testNpubA,
),
),
],
),
],
),
),
isOwnMessage: false,
groupId: testGroupId,
),
);
final span = tester.widget<RichText>(find.byType(RichText).first).text as TextSpan;
final entity = _firstTextSpanWithRecognizer(span);
(entity.recognizer as TapGestureRecognizer).onTap!();
await tester.pumpAndSettle();
expect(launcher.calls, isEmpty);
expect(find.byType(StartChatScreen), findsNothing);
expect(
find.byKey(const ValueKey('group_member_route_${testGroupId}_$testPubkeyA')),
findsOneWidget,
);
});
testWidgets('whitenoise://chat/<id> tap navigates via GoRouter', (tester) async {
await _mountBubbleWithRouter(
tester,
@@ -1075,6 +1111,16 @@ Future<void> _mountBubbleWithRouter(WidgetTester tester, Widget bubble) async {
body: const SizedBox(),
),
),
GoRoute(
name: 'groupMember',
path: '/group-member/:groupId/:memberPubkey',
builder: (_, state) => Scaffold(
key: ValueKey(
'group_member_route_${state.pathParameters['groupId']}_${state.pathParameters['memberPubkey']}',
),
body: const SizedBox(),
),
),
],
);
await tester.pumpWidget(