Tapping on comment links navigate inside the app

This commit is contained in:
franzap
2026-04-08 15:13:58 -03:00
parent eec8f7822d
commit 41b5fba40b
2 changed files with 70 additions and 19 deletions
+17 -3
View File
@@ -1,6 +1,7 @@
import 'package:async_button_builder/async_button_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:models/models.dart';
@@ -8,6 +9,7 @@ import 'package:zapstore/services/notification_service.dart';
import 'package:zapstore/theme.dart';
import 'package:zapstore/utils/extensions.dart';
import 'package:zapstore/widgets/auth_widgets.dart';
import 'package:zapstore/widgets/common/note_parser.dart';
import 'package:zapstore/widgets/common/profile_avatar.dart';
import 'package:zapstore/widgets/common/profile_name_widget.dart';
import 'package:zapstore/widgets/pill_widget.dart';
@@ -387,13 +389,25 @@ class _ThreadedCommentCard extends HookConsumerWidget {
],
),
const SizedBox(height: 6),
Text(
NoteParser.parse(
context,
comment.content,
style: context.textTheme.bodyMedium?.copyWith(
textStyle: context.textTheme.bodyMedium?.copyWith(
fontSize: isReply ? 13 : null,
),
onNostrEntity: (entity) => NostrEntityWidget(
entity: entity,
colorPair: [
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.secondary,
],
onProfileTap: (pubkey) {
final segments = GoRouterState.of(context).uri.pathSegments;
final branch = segments.isNotEmpty ? segments.first : 'search';
context.push('/$branch/user/$pubkey');
},
),
),
// Reply button
if (isSignedIn) ...[
const SizedBox(height: 6),
GestureDetector(
+53 -16
View File
@@ -1,4 +1,6 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:models/models.dart';
import 'package:zapstore/utils/extensions.dart';
@@ -156,13 +158,21 @@ class NoteParser {
);
}
// Sort matches by position
// Sort matches by position and remove overlaps (e.g. #fragment inside a URL)
matches.sort((a, b) => a.start.compareTo(b.start));
final filtered = <_EntityMatch>[];
var lastEnd = -1;
for (final m in matches) {
if (m.start >= lastEnd) {
filtered.add(m);
lastEnd = m.end;
}
}
// Build spans
int currentPos = 0;
for (final match in matches) {
for (final match in filtered) {
// Add text before this match
if (match.start > currentPos) {
final textBefore = content.substring(currentPos, match.start);
@@ -207,13 +217,24 @@ class NoteParser {
baseline: TextBaseline.alphabetic,
),
);
} else {
// Fallback to styled text
} else if (match.type == _EntityType.http ||
match.type == _EntityType.media) {
final style =
linkStyle ??
textStyle?.copyWith(
color: Theme.of(context).colorScheme.primary,
);
spans.add(TextSpan(
text: match.text,
style: style,
recognizer: TapGestureRecognizer()
..onTap = () => _launchUrlSafely(match.text),
));
} else {
final style =
linkStyle ??
textStyle?.copyWith(
color: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline,
);
spans.add(TextSpan(text: match.text, style: style));
}
@@ -397,21 +418,26 @@ class ProfileEntityWidget extends ConsumerWidget {
),
);
// Show animated npub while profile is being loaded
void handleTap() {
if (onProfileTap != null) {
onProfileTap!(profileData.pubkey);
} else {
final segments = GoRouterState.of(context).uri.pathSegments;
final branch = segments.isNotEmpty ? segments.first : 'search';
context.push('/$branch/user/${profileData.pubkey}');
}
}
return switch (profileState) {
StorageLoading() => GestureDetector(
onTap: onProfileTap != null
? () => onProfileTap!(profileData.pubkey)
: null,
onTap: handleTap,
child: _AnimatedLoadingChip(
text: 'npub1${profileData.pubkey.substring(0, 8)}...',
colorPair: colorPair,
),
),
StorageError() || StorageData(models: []) => GestureDetector(
onTap: onProfileTap != null
? () => onProfileTap!(profileData.pubkey)
: null,
onTap: handleTap,
child: _AnimatedLoadingChip(
text: 'npub1${profileData.pubkey.substring(0, 8)}...',
colorPair: colorPair,
@@ -424,10 +450,18 @@ class ProfileEntityWidget extends ConsumerWidget {
Widget _buildProfileWidget(BuildContext context, Profile profile) {
final displayName = profile.nameOrNpub;
void handleTap() {
if (onProfileTap != null) {
onProfileTap!(profileData.pubkey);
} else {
final segments = GoRouterState.of(context).uri.pathSegments;
final branch = segments.isNotEmpty ? segments.first : 'search';
context.push('/$branch/user/${profileData.pubkey}');
}
}
return GestureDetector(
onTap: onProfileTap != null
? () => onProfileTap!(profileData.pubkey)
: null,
onTap: handleTap,
child: DecoratedBox(
decoration: BoxDecoration(
color: colorPair[0].withValues(alpha: 0.1),
@@ -531,7 +565,10 @@ class AddressEntityWidget extends StatelessWidget {
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
// Handle address navigation
final segment = addressData.kind == 30267 ? 'stack' : 'app';
final segments = GoRouterState.of(context).uri.pathSegments;
final branch = segments.isNotEmpty ? segments.first : 'search';
context.push('/$branch/$segment/${addressData.identifier}');
},
child: DecoratedBox(
decoration: BoxDecoration(