general fixups, plus adding colors for names

This commit is contained in:
Henry Hiles 2026-05-20 16:06:17 -04:00
commit cff580dee2
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
14 changed files with 196 additions and 122 deletions

View file

@ -1,6 +1,7 @@
import "dart:async";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/user_controller.dart";
import "package:nexus/models/configs/user_config.dart";
import "package:nexus/models/content/membership.dart";
import "package:nexus/models/event.dart";
@ -11,7 +12,9 @@ class AuthorController extends AsyncNotifier<MembershipContent> {
@override
Future<MembershipContent> build() async {
final member = await ref.watch(
UserController.provider(event.sender).future,
UserController.provider(
UserConfig(roomId: event.roomId, userId: event.sender),
).future,
);
return MembershipContent(

View file

@ -4,25 +4,44 @@ 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/configs/user_config.dart";
import "package:nexus/models/content/membership.dart";
import "package:nexus/models/membership_status.dart";
class UserController extends AsyncNotifier<MembershipContent> {
final String userId;
UserController(this.userId);
final UserConfig config;
UserController(this.config);
@override
Future<MembershipContent> build() async {
final profile = await ref.watch(ProfileController.provider(userId).future);
final member = config.roomId == null
? null
: await ref.watch(
MembersController.provider(config.roomId!).selectAsync(
(value) => value.firstWhereOrNull(
(membership) => membership.stateKey == config.userId,
),
),
);
if (member?.content case final MembershipContent content) {
return content;
}
final profile = await ref.watch(
ProfileController.provider(config.userId).future,
);
return MembershipContent(
status: MembershipStatus.leave,
avatarUrl: profile.avatarUrl,
displayName: profile.displayName ?? userId.localpart,
displayName: profile.displayName ?? config.userId.localpart,
);
}
static final provider =
AsyncNotifierProvider.family<UserController, MembershipContent, String>(
UserController.new,
);
AsyncNotifierProvider.family<
UserController,
MembershipContent,
UserConfig
>(UserController.new);
}

View file

@ -67,8 +67,8 @@ void main() async {
await windowManager.setMinimumSize(Size.square(500));
}
FlutterError.onError = (FlutterErrorDetails details) =>
showError(details.exception.toString(), details.stack);
// FlutterError.onError = (FlutterErrorDetails details) =>
// showError(details.exception.toString(), details.stack);
runApp(
ProviderScope(

View file

@ -0,0 +1,12 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "user_config.freezed.dart";
part "user_config.g.dart";
@freezed
abstract class UserConfig with _$UserConfig {
const factory UserConfig({required String? roomId, required String userId}) =
_UserConfig;
factory UserConfig.fromJson(Map<String, Object?> json) =>
_$UserConfigFromJson(json);
}

View file

@ -2,9 +2,7 @@ import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/relation_type.dart";
import "package:nexus/widgets/renderers/event.dart";
import "package:nexus/widgets/lazy_loading/message_avatar.dart";
import "package:nexus/widgets/lazy_loading/message_displayname.dart";
import "package:nexus/widgets/event_preview.dart";
class RelationPreview extends ConsumerWidget {
final Event? relatedEvent;
@ -29,7 +27,7 @@ class RelationPreview extends ConsumerWidget {
return Container(
color: theme.colorScheme.surfaceContainerHigh,
padding: EdgeInsets.symmetric(horizontal: 8),
padding: EdgeInsets.symmetric(horizontal: 12),
child: Row(
spacing: 8,
children: [
@ -39,30 +37,10 @@ class RelationPreview extends ConsumerWidget {
style: TextStyle(fontWeight: FontWeight.bold),
),
MessageAvatar(relatedEvent!),
Expanded(
child: Row(
spacing: 8,
children: [
Flexible(
child: MessageDisplayname(
relatedEvent!,
style: theme.textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
Expanded(
child: IgnorePointer(
child: EventRenderer(
relatedEvent!,
textOnly: true,
maxLines: 1,
),
),
),
],
child: Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: EventPreview(relatedEvent!),
),
),

View file

@ -0,0 +1,36 @@
import "package:flutter/material.dart";
import "package:nexus/models/content/message.dart";
import "package:nexus/models/event.dart";
import "package:nexus/widgets/lazy_loading/message_avatar.dart";
import "package:nexus/widgets/lazy_loading/message_displayname.dart";
import "package:nexus/widgets/renderers/event.dart";
class EventPreview extends StatelessWidget {
final Event event;
const EventPreview(this.event, {super.key});
@override
Widget build(BuildContext context) => IgnorePointer(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 4),
child: Row(
spacing: 12,
children: [
if (event.content is MessageContent) MessageAvatar(event),
Expanded(
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 4,
runSpacing: 2,
children: [
if (event.content is MessageContent) MessageDisplayname(event),
EventRenderer(event, textOnly: true, maxLines: 1),
],
),
),
],
),
),
);
}

View file

@ -17,8 +17,9 @@ import "package:nexus/widgets/html/quoted.dart";
class Html extends ConsumerWidget {
final String html;
final String? roomId;
final TextStyle? textStyle;
const Html(this.html, {this.textStyle, super.key});
const Html(this.html, {this.roomId, this.textStyle, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) => HtmlWidget(
@ -59,13 +60,15 @@ class Html extends ConsumerWidget {
)
: null,
"blockquote" => Quoted(Html(element.innerHtml)),
"blockquote" => Quoted(
Html(element.innerHtml, textStyle: textStyle, roomId: roomId),
),
"a" =>
element.attributes["href"]?.mention == null
? null
: InlineCustomWidget(
child: MentionChip(element.attributes["href"]!),
child: MentionChip(element.attributes["href"]!, roomId),
),
"img" =>

View file

@ -3,17 +3,23 @@ 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";
import "package:nexus/models/configs/user_config.dart";
class MentionChip extends ConsumerWidget {
final String? roomId;
final String content;
const MentionChip(this.content, {super.key});
const MentionChip(this.content, this.roomId, {super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final mention = content.mention;
final membership = mention?.startsWith("@") == true
? ref
.watch(UserController.provider(mention!))
.watch(
UserController.provider(
UserConfig(roomId: roomId, userId: mention!),
),
)
.whenOrNull(data: (data) => data)
: null;

View file

@ -10,7 +10,7 @@ import "package:nexus/widgets/avatar_or_hash.dart";
class MessageAvatar extends ConsumerWidget {
final Event event;
final double height;
const MessageAvatar(this.event, {this.height = 16, super.key});
const MessageAvatar(this.event, {this.height = 24, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) => ref

View file

@ -1,3 +1,4 @@
import "package:color_hash/color_hash.dart";
import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/author_controller.dart";
@ -31,7 +32,17 @@ class MessageDisplayname extends ConsumerWidget {
: null,
child: Text(
"${membership.displayName ?? event.sender.localpart}${event.pmp == null ? "" : " (via ${event.sender})"}",
style: style,
style:
style ??
TextStyle(
color: ColorHash(
event.sender,
lightness: .7,
saturation: .7,
).color,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),

View file

@ -1,14 +1,16 @@
import "package:color_hash/color_hash.dart";
import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/members_by_status_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/helpers/extensions/get_localpart.dart";
import "package:nexus/helpers/extensions/show_user_popover.dart";
import "package:nexus/models/configs/members_by_status_config.dart";
import "package:nexus/models/content/membership.dart";
import "package:nexus/models/membership_status.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/error_dialog.dart";
import "package:nexus/widgets/loading.dart";
class MemberList extends HookConsumerWidget {
final String roomId;
@ -63,10 +65,14 @@ class MemberList extends HookConsumerWidget {
),
],
),
membersProvider.betterWhen(
data: (members) => Expanded(
switch (membersProvider) {
AsyncError(:final error, :final stackTrace) => ErrorDialog(
error,
stackTrace,
),
AsyncData(:final value) || AsyncLoading(:final value?) => Expanded(
child: ListView(
children: members
children: value
.map(
(member) => switch (member.content) {
MembershipContent(
@ -87,6 +93,14 @@ class MemberList extends HookConsumerWidget {
title: Text(
displayName ?? member.stateKey!.localpart,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: ColorHash(
member.stateKey!,
lightness: .7,
saturation: .8,
).color,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
member.stateKey!,
@ -100,7 +114,8 @@ class MemberList extends HookConsumerWidget {
.toList(),
),
),
),
AsyncLoading _ => Loading(),
},
],
),
);

View file

@ -19,6 +19,7 @@ import "package:nexus/models/content/membership.dart";
import "package:nexus/models/content/message.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/requests/get_event_request.dart";
import "package:nexus/widgets/event_preview.dart";
import "package:nexus/widgets/expandable_image.dart";
import "package:nexus/widgets/html/html.dart";
import "package:nexus/widgets/lazy_loading/message_avatar.dart";
@ -59,6 +60,8 @@ class EventRenderer extends ConsumerWidget {
message: event.timestamp.toString(),
child: Text(
format(event.timestamp),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.labelSmall?.copyWith(color: Colors.grey),
),
);
@ -83,6 +86,7 @@ class EventRenderer extends ConsumerWidget {
),
MessageContent() || EncryptedContent() => Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
if (!textOnly)
@ -90,7 +94,7 @@ class EventRenderer extends ConsumerWidget {
SizedBox(width: 40)
else
MessageAvatar(event, height: 40),
Expanded(
Flexible(
child: Column(
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.start,
@ -99,16 +103,14 @@ class EventRenderer extends ConsumerWidget {
Row(
spacing: 4,
children: [
Flexible(
child: MessageDisplayname(
event,
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Flexible(child: timestamp),
Flexible(child: MessageDisplayname(event)),
Flexible(flex: 0, child: timestamp),
],
),
Card(
margin: textOnly
? EdgeInsets.zero
: EdgeInsets.only(bottom: 4),
color: textOnly
? Colors.transparent
: ref.watch(
@ -152,32 +154,7 @@ class EventRenderer extends ConsumerWidget {
AsyncData(:final value?) ||
AsyncLoading(
:final value?,
) => IgnorePointer(
child: Row(
spacing: 8,
children: [
MessageAvatar(value, height: 24),
Flexible(
child: MessageDisplayname(
value,
style: TextStyle(
color: theme
.colorScheme
.primary,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
child: EventRenderer(
value,
textOnly: true,
maxLines: 1,
),
),
],
),
),
) => EventPreview(value),
AsyncError _ => Text(
"An error occurred while fetching the reply",
style: errorStyle,
@ -233,6 +210,7 @@ class EventRenderer extends ConsumerWidget {
children: [
format == MessageFormat.html && !textOnly
? Html(
roomId: event.roomId,
textStyle: textStyle,
formattedBody!.replaceAllMapped(
RegExp(
@ -291,7 +269,7 @@ class EventRenderer extends ConsumerWidget {
)) {
final url? => ConstrainedBox(
constraints: BoxConstraints.loose(
Size.fromWidth(500),
Size.square(500),
),
child: switch (event.content) {
VideoMessageContent(
@ -425,14 +403,8 @@ class EventRenderer extends ConsumerWidget {
padding: EdgeInsets.symmetric(horizontal: 4),
child: Icon(Icons.numbers),
),
MessageDisplayname(
event,
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
Text("changed the room avatar"),
Flexible(child: MessageDisplayname(event)),
Expanded(child: Text("changed the room avatar")),
],
),
_ => null,

View file

@ -19,12 +19,16 @@ class MembershipRenderer extends StatelessWidget {
return switch (event.content) {
MembershipContent content => Row(
spacing: 4,
spacing: 8,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: Icon(Icons.people),
),
Expanded(
child: Wrap(
spacing: 4,
children: [
InkWell(
onTapUp: (details) => context.showUserPopover(
content,
@ -32,7 +36,9 @@ class MembershipRenderer extends StatelessWidget {
globalPosition: details.globalPosition,
),
child: Text(
overflow: TextOverflow.ellipsis,
content.displayName ?? event.stateKey!.localpart,
maxLines: 1,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
@ -40,6 +46,8 @@ class MembershipRenderer extends StatelessWidget {
),
),
Text(
overflow: TextOverflow.ellipsis,
maxLines: 1,
"${switch (content.status) {
MembershipStatus.invite => "was invited to",
MembershipStatus.join => "joined",
@ -58,6 +66,9 @@ class MembershipRenderer extends StatelessWidget {
),
],
),
),
],
),
_ => SizedBox.shrink(),
};
}

View file

@ -73,6 +73,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
boxy:
dependency: "direct main"
description:
name: boxy
sha256: "42ccafe13b2893878042acc5b7e2446025328e11a3197b0bb78db42ff76aa3f0"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
build:
dependency: transitive
description: