dont fetch members on load

This commit is contained in:
Henry Hiles 2026-03-22 14:30:46 -04:00
commit 8b056d8ed1
No known key found for this signature in database
21 changed files with 206 additions and 137 deletions

View file

@ -0,0 +1,42 @@
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/models/configs/member_config.dart";
import "package:nexus/models/membership.dart";
class MemberController extends AsyncNotifier<Membership> {
final MemberConfig config;
MemberController(this.config);
@override
FutureOr<Membership> build() {
final member = ref.watch(
MembersController.provider(config.room).select(
(value) => value.firstWhereOrNull(
(membership) => membership.userId == config.userId,
),
),
);
if (config.room.hasFetchedMembers || member != null) {
return member ??
Membership(
avatarUrl: null,
displayName: config.userId,
userId: config.userId,
);
}
return Membership(
avatarUrl: null,
displayName: config.userId,
userId: config.userId,
);
throw UnimplementedError();
}
static final provider = AsyncNotifierProvider.family
.autoDispose<MemberController, Membership, MemberConfig>(
MemberController.new,
);
}

View file

@ -1,25 +1,32 @@
import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/membership.dart";
import "package:nexus/models/room.dart";
class MembersController extends Notifier<IList<Event>> {
class MembersController extends Notifier<IList<Membership>> {
final Room room;
MembersController(this.room);
@override
IList<Event> build() => (room.state["m.room.member"]?.values ?? [])
IList<Membership> build() => (room.state["m.room.member"]?.values ?? [])
.map(
(eventRowId) =>
room.events.firstWhereOrNull((event) => event.rowId == eventRowId),
)
.nonNulls
.where((member) => member.content["membership"] == "join")
.map(
(membership) => Membership(
avatarUrl: Uri.tryParse(membership.content["avatar_url"] ?? ""),
userId: membership.stateKey!,
displayName: membership.content["displayname"] ?? membership.stateKey,
),
)
.toIList();
static final provider = NotifierProvider.family
.autoDispose<MembersController, IList<Event>, Room>(
.autoDispose<MembersController, IList<Membership>, Room>(
MembersController.new,
);
}

View file

@ -2,9 +2,8 @@ import "package:collection/collection.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/helpers/extensions/mxc_to_https.dart";
import "package:nexus/models/message_config.dart";
import "package:nexus/models/configs/message_config.dart";
class MessageController extends AsyncNotifier<Message?> {
final MessageConfig config;
@ -27,12 +26,6 @@ class MessageController extends AsyncNotifier<Message?> {
if (!ref.mounted) return null;
final members = ref.read(MembersController.provider(config.room));
final author = members.firstWhereOrNull(
(member) => member.stateKey == event.authorId,
);
if (!ref.mounted) return null;
final content = (event.decrypted ?? event.content);
final type = (config.event.decryptedType ?? config.event.type);
final newContent = content["m.new_content"] as Map?;
@ -52,14 +45,10 @@ class MessageController extends AsyncNotifier<Message?> {
"timelineId": event.timelineRowId,
"big": event.localContent?.bigEmoji == true,
"eventType": type,
"avatarUrl": author?.content["avatar_url"],
"editSource":
event.localContent?.editSource ??
newContent?["body"] ??
content["body"],
"displayName": author?.content["displayname"]?.isNotEmpty == true
? author?.content["displayname"]
: event.authorId.substring(1).split(":")[0],
"txnId": config.event.transactionId,
};

View file

@ -2,8 +2,8 @@ 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/message_controller.dart";
import "package:nexus/models/message_config.dart";
import "package:nexus/models/messages_config.dart";
import "package:nexus/models/configs/message_config.dart";
import "package:nexus/models/configs/messages_config.dart";
class MessagesController extends AsyncNotifier<IList<Message>> {
final MessagesConfig config;

View file

@ -1,5 +1,4 @@
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";
@ -11,8 +10,8 @@ import "package:nexus/controllers/message_controller.dart";
import "package:nexus/controllers/messages_controller.dart";
import "package:nexus/controllers/new_events_controller.dart";
import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/models/message_config.dart";
import "package:nexus/models/messages_config.dart";
import "package:nexus/models/configs/messages_config.dart";
import "package:nexus/models/configs/message_config.dart";
import "package:nexus/models/requests/get_room_state_request.dart";
import "package:nexus/models/requests/paginate_request.dart";
import "package:nexus/models/requests/redact_event_request.dart";
@ -31,11 +30,7 @@ class RoomChatController extends AsyncNotifier<InMemoryChatController> {
if (room == null) return InMemoryChatController();
final state = await client.getRoomState(
GetRoomStateRequest(
roomId: roomId,
fetchMembers: room.metadata?.hasMemberList == false,
includeMembers: true,
),
GetRoomStateRequest(roomId: roomId),
);
ref

View file

@ -0,0 +1,13 @@
import "package:freezed_annotation/freezed_annotation.dart";
import "package:nexus/models/room.dart";
part "member_config.freezed.dart";
part "member_config.g.dart";
@freezed
abstract class MemberConfig with _$MemberConfig {
const factory MemberConfig({required Room room, required String userId}) =
_MemberConfig;
factory MemberConfig.fromJson(Map<String, Object?> json) =>
_$MemberConfigFromJson(json);
}

View file

@ -0,0 +1,15 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "membership.freezed.dart";
part "membership.g.dart";
@freezed
abstract class Membership with _$Membership {
const factory Membership({
required Uri? avatarUrl,
required String displayName,
required String userId,
}) = _Membership;
factory Membership.fromJson(Map<String, Object?> json) =>
_$MembershipFromJson(json);
}

View file

@ -6,7 +6,7 @@ part "get_room_state_request.g.dart";
abstract class GetRoomStateRequest with _$GetRoomStateRequest {
const factory GetRoomStateRequest({
required String roomId,
required bool fetchMembers,
@Default(false) bool fetchMembers,
@Default(false) bool includeMembers,
}) = _GetRoomStateRequest;

View file

@ -18,6 +18,7 @@ abstract class Room with _$Room {
@Default(IMap.empty()) IMap<String, IList<ReadReceipt>> receipts,
@Default(false) bool dismissNotifications,
@Default(true) bool hasMore,
@Default(false) bool hasFetchedMembers,
// required IList<Notification> notifications,
}) = _Room;

View file

@ -86,10 +86,11 @@ class ChatBox extends HookConsumerWidget {
child: Column(
children: [
RelationPreview(
relatedMessage,
room: room,
shouldMention: shouldMention.value,
toggleShouldMention: () =>
shouldMention.value = !shouldMention.value,
relatedMessage: relatedMessage,
relationType: relationType,
onDismiss: onDismiss,
),

View file

@ -0,0 +1,32 @@
import "package:flutter/widgets.dart";
import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/member_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/models/configs/member_config.dart";
import "package:nexus/models/room.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
class MessageAvatar extends ConsumerWidget {
final Message message;
final Room room;
final double height;
const MessageAvatar(this.message, this.room, {this.height = 16, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) => ref
.watch(
MemberController.provider(
MemberConfig(room: room, userId: message.authorId),
),
)
.betterWhen(
data: (membership) => AvatarOrHash(
membership.avatarUrl,
membership.displayName,
height: height,
),
loading: () =>
AvatarOrHash(null, message.authorId.substring(1), height: height),
);
}

View file

@ -0,0 +1,30 @@
import "package:flutter/widgets.dart";
import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/member_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/models/configs/member_config.dart";
import "package:nexus/models/room.dart";
class MessageDisplayname extends ConsumerWidget {
final Message message;
final Room room;
final TextStyle? style;
const MessageDisplayname(this.message, this.room, {this.style, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) => ref
.watch(
MemberController.provider(
MemberConfig(room: room, userId: message.authorId),
),
)
.betterWhen(
data: (membership) => Text(
membership.displayName,
style: style,
overflow: TextOverflow.ellipsis,
),
loading: SizedBox.shrink,
);
}

View file

@ -36,18 +36,9 @@ class MemberList extends ConsumerWidget {
builder: (context) =>
Dialog(child: Text("TODO: Open member popover")),
),
leading: AvatarOrHash(
Uri.tryParse(member.content["avatar_url"] ?? ""),
member.content["displayname"].toString(),
),
title: Text(
member.content["displayname"].toString(),
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
member.stateKey ?? "Unknown User",
overflow: TextOverflow.ellipsis,
),
leading: AvatarOrHash(member.avatarUrl, member.displayName),
title: Text(member.displayName, overflow: TextOverflow.ellipsis),
subtitle: Text(member.userId, overflow: TextOverflow.ellipsis),
),
),
],

View file

@ -40,39 +40,29 @@ class MentionOverlay extends ConsumerWidget {
? members
: members.where(
(member) =>
member.stateKey?.toLowerCase().contains(
member.userId.toLowerCase().contains(
query.toLowerCase(),
) ==
true ||
(member.content["displayname"] as String?)
?.toLowerCase()
.contains(query.toLowerCase()) ==
member.displayName.toLowerCase().contains(
query.toLowerCase(),
) ==
true,
))
.map(
(member) => ListTile(
leading: AvatarOrHash(
Uri.tryParse(
member.content["avatar_url"] ?? "",
),
member.content["displayname"] ?? "",
member.avatarUrl,
member.displayName,
),
title: Text(
member.content["displayname"] as String? ??
member.stateKey ??
"Unknown User",
),
subtitle: member.stateKey != null
? Text(member.stateKey!)
: null,
title: Text(member.displayName),
subtitle: Text(member.userId),
onTap: () => addTag(
id: "[@${member.content["displayname"]}](https://matrix.to/#/${member.stateKey})",
name:
member.stateKey
?.substring(1)
.split(":")
.first ??
"Unknown User",
id: "[@${member.displayName}](https://matrix.to/#/${member.userId})",
name: member.userId
.substring(1)
.split(":")
.first,
),
),
)

View file

@ -1,12 +1,21 @@
import "package:flutter/material.dart";
import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/models/room.dart";
import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart";
import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart";
class MessageWrapper extends StatelessWidget {
final Message message;
final Widget child;
final Room room;
final MessageGroupStatus? groupStatus;
const MessageWrapper(this.message, this.child, this.groupStatus, {super.key});
const MessageWrapper(
this.message,
this.child,
this.groupStatus,
this.room, {
super.key,
});
@override
Widget build(BuildContext context) => ClipRRect(
@ -24,11 +33,7 @@ class MessageWrapper extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
groupStatus?.isFirst != false
? AvatarOrHash(
Uri.parse(message.metadata?["avatarUrl"] ?? ""),
height: 40,
message.metadata?["displayName"] ?? "",
)
? MessageAvatar(message, room, height: 40)
: SizedBox(width: 40),
Expanded(
child: Column(
@ -36,9 +41,9 @@ class MessageWrapper extends StatelessWidget {
spacing: 4,
children: [
if (groupStatus?.isFirst != false)
Text(
message.metadata?["displayName"] ?? message.authorId,
overflow: TextOverflow.ellipsis,
MessageDisplayname(
message,
room,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),

View file

@ -2,7 +2,9 @@ import "package:flutter/material.dart";
import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/models/relation_type.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/models/room.dart";
import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart";
import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart";
class RelationPreview extends ConsumerWidget {
final Message? relatedMessage;
@ -10,8 +12,11 @@ class RelationPreview extends ConsumerWidget {
final VoidCallback onDismiss;
final bool shouldMention;
final VoidCallback toggleShouldMention;
const RelationPreview({
required this.relatedMessage,
final Room room;
const RelationPreview(
this.relatedMessage, {
required this.room,
required this.relationType,
required this.onDismiss,
required this.shouldMention,
@ -36,14 +41,10 @@ class RelationPreview extends ConsumerWidget {
"Editing message:",
style: TextStyle(fontWeight: FontWeight.bold),
),
AvatarOrHash(
Uri.tryParse(relatedMessage?.metadata?["avatarUrl"] ?? ""),
relatedMessage?.metadata?["displayName"]?.toString() ?? "",
height: 16,
),
Text(
relatedMessage!.metadata?["displayName"] ??
relatedMessage!.authorId,
MessageAvatar(relatedMessage!, room),
MessageDisplayname(
relatedMessage!,
room,
style: theme.textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.bold,
),

View file

@ -1,15 +1,15 @@
import "dart:math";
import "package:flutter/material.dart";
import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/event_controller.dart";
import "package:nexus/controllers/message_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/models/message_config.dart";
import "package:nexus/models/configs/message_config.dart";
import "package:nexus/models/requests/get_event_request.dart";
import "package:nexus/models/room.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/chat_page/html/quoted.dart";
import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart";
import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart";
typedef OnTapReply = void Function(Message message)?;
@ -61,73 +61,28 @@ class ReplyWidget extends ConsumerWidget {
return SizedBox.shrink();
}
final smallerText =
message is TextMessage &&
replyMessage.metadata?["body"] != null
? replyMessage.metadata!["body"].substring(
0,
min(
max(
max(
(message as TextMessage)
.text
.length -
(replyMessage
.metadata?["displayName"]
as String)
.length -
5,
message
.metadata?["displayName"]
.length,
),
5,
),
replyMessage.metadata!["body"].length,
),
)
: null;
final replyText =
(smallerText == null ||
smallerText.length ==
replyMessage
.metadata!["body"]
.length)
? replyMessage.metadata!["body"]
: "$smallerText...";
return InkWell(
onTap: () => onTapReply?.call(replyMessage),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
AvatarOrHash(
Uri.tryParse(
replyMessage.metadata?["avatarUrl"] ??
"",
),
replyMessage.metadata?["displayName"] ??
"",
height: 16,
),
MessageAvatar(message, room),
Flexible(
child: Text(
replyMessage
.metadata?["displayName"] ??
replyMessage.authorId,
child: MessageDisplayname(
replyMessage,
room,
style: Theme.of(context)
.textTheme
.labelMedium
?.copyWith(
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
Flexible(
child: Text(
replyText,
replyMessage.metadata!["body"],
overflow: TextOverflow.ellipsis,
style: Theme.of(
context,

View file

@ -320,6 +320,7 @@ class RoomChat extends HookConsumerWidget {
),
),
groupStatus,
room,
),
systemMessageBuilder:

View file

@ -109,6 +109,7 @@ class TextMessageWrapper extends StatelessWidget {
),
),
groupStatus,
room,
);
}
}