clickable user mentions

This commit is contained in:
Henry Hiles 2026-04-04 11:34:40 -04:00
commit d70c439278
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
5 changed files with 83 additions and 26 deletions

View file

@ -3,6 +3,7 @@
"Appbar", "Appbar",
"Displayname", "Displayname",
"Homeserver", "Homeserver",
"localpart",
"prefs", "prefs",
"vodozemac" "vodozemac"
] ]

View file

@ -1,10 +1,10 @@
import "dart:async"; import "dart:async";
import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/controllers/members_controller.dart"; import "package:nexus/controllers/user_controller.dart";
import "package:nexus/helpers/extensions/get_localpart.dart";
import "package:nexus/models/membership.dart"; import "package:nexus/models/membership.dart";
import "package:nexus/models/membership_status.dart"; import "package:nexus/models/membership_status.dart";
@ -15,11 +15,7 @@ class AuthorController extends AsyncNotifier<Membership> {
@override @override
Future<Membership> build() async { Future<Membership> build() async {
final member = await ref.watch( final member = await ref.watch(
MembersController.provider.selectAsync( UserController.provider(message.authorId).future,
(value) => value.firstWhereOrNull(
(membership) => membership.userId == message.authorId,
),
),
); );
final pmp = message.metadata?["pmp"] == null final pmp = message.metadata?["pmp"] == null
@ -39,9 +35,7 @@ class AuthorController extends AsyncNotifier<Membership> {
status: member?.status ?? MembershipStatus.leave, status: member?.status ?? MembershipStatus.leave,
avatarUrl: pmp?.avatarUrl ?? member?.avatarUrl, avatarUrl: pmp?.avatarUrl ?? member?.avatarUrl,
displayName: displayName:
pmp?.displayName ?? pmp?.displayName ?? member?.displayName ?? message.authorId.localpart,
member?.displayName ??
message.authorId.substring(1).split(":").first,
userId: message.authorId, userId: message.authorId,
); );
} }

View file

@ -0,0 +1,40 @@
import "dart:async";
import "package:collection/collection.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/members_controller.dart";
import "package:nexus/controllers/profile_controller.dart";
import "package:nexus/helpers/extensions/get_localpart.dart";
import "package:nexus/models/membership.dart";
import "package:nexus/models/membership_status.dart";
class UserController extends AsyncNotifier<Membership?> {
final String userId;
UserController(this.userId);
@override
Future<Membership?> build() async {
final member = await ref.watch(
MembersController.provider.selectAsync(
(value) =>
value.firstWhereOrNull((membership) => membership.userId == userId),
),
);
if (member != null) return member;
final profile = await ref.watch(ProfileController.provider(userId).future);
return Membership(
status: MembershipStatus.leave,
avatarUrl: profile.avatarUrl == null
? null
: Uri.tryParse(profile.avatarUrl!),
displayName: profile.displayName ?? userId.localpart,
userId: userId,
);
}
static final provider =
AsyncNotifierProvider.family<UserController, Membership?, String>(
UserController.new,
);
}

View file

@ -0,0 +1,3 @@
extension GetLocalpart on String {
String get localpart => substring(1).split(":").first;
}

View file

@ -1,25 +1,44 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/user_controller.dart";
import "package:nexus/helpers/extensions/link_to_mention.dart"; import "package:nexus/helpers/extensions/link_to_mention.dart";
import "package:nexus/helpers/extensions/show_user_popover.dart";
class MentionChip extends StatelessWidget { class MentionChip extends ConsumerWidget {
final String label; final String label;
const MentionChip(this.label, {super.key}); const MentionChip(this.label, {super.key});
@override @override
Widget build(BuildContext context) => ActionChip( Widget build(BuildContext context, WidgetRef ref) {
label: Text( final mention = label.mention;
label.mention ?? label, final membership =
style: TextStyle( mention?.startsWith("@") == true || label.startsWith("@") == true
fontWeight: FontWeight.bold, ? ref
color: Theme.of(context).colorScheme.onPrimary, .watch(UserController.provider(mention ?? label))
.whenOrNull(data: (data) => data)
: null;
return InkWell(
onTapUp: (details) {
if (membership != null) {
context.showUserPopover(
membership,
globalPosition: details.globalPosition,
);
}
},
child: Chip(
label: Text(
(membership == null ? null : "@${membership.displayName}") ??
mention ??
label,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onPrimary,
),
),
backgroundColor: Theme.of(context).colorScheme.primary,
), ),
), );
backgroundColor: Theme.of(context).colorScheme.primary, }
onPressed: () => showDialog(
context: context,
builder: (_) => Dialog(
child: Text("TODO: Open room or join room dialog, or user popover"),
),
),
);
} }