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",
"Displayname",
"Homeserver",
"localpart",
"prefs",
"vodozemac"
]

View file

@ -1,10 +1,10 @@
import "dart:async";
import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_riverpod/flutter_riverpod.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_status.dart";
@ -15,11 +15,7 @@ class AuthorController extends AsyncNotifier<Membership> {
@override
Future<Membership> build() async {
final member = await ref.watch(
MembersController.provider.selectAsync(
(value) => value.firstWhereOrNull(
(membership) => membership.userId == message.authorId,
),
),
UserController.provider(message.authorId).future,
);
final pmp = message.metadata?["pmp"] == null
@ -39,9 +35,7 @@ class AuthorController extends AsyncNotifier<Membership> {
status: member?.status ?? MembershipStatus.leave,
avatarUrl: pmp?.avatarUrl ?? member?.avatarUrl,
displayName:
pmp?.displayName ??
member?.displayName ??
message.authorId.substring(1).split(":").first,
pmp?.displayName ?? member?.displayName ?? message.authorId.localpart,
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_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/show_user_popover.dart";
class MentionChip extends StatelessWidget {
class MentionChip extends ConsumerWidget {
final String label;
const MentionChip(this.label, {super.key});
@override
Widget build(BuildContext context) => ActionChip(
label: Text(
label.mention ?? label,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onPrimary,
Widget build(BuildContext context, WidgetRef ref) {
final mention = label.mention;
final membership =
mention?.startsWith("@") == true || label.startsWith("@") == true
? ref
.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"),
),
),
);
);
}
}