chore: Adjust design of user viewer and popup buttons

This commit is contained in:
Christian Kußowski 2026-02-24 21:28:33 +01:00
parent 2408568f36
commit 0052a15b54
No known key found for this signature in database
GPG key ID: E067ECD60F1A0652
3 changed files with 181 additions and 162 deletions

View file

@ -440,25 +440,16 @@ class ChatListController extends State<ChatList>
PopupMenuItem( PopupMenuItem(
value: ChatContextAction.open, value: ChatContextAction.open,
child: Row( child: Row(
mainAxisSize: .min,
spacing: 12.0, spacing: 12.0,
children: [ children: [
Avatar(mxContent: room.avatar, name: displayname), Avatar(mxContent: room.avatar, name: displayname, size: 24),
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 128), constraints: const BoxConstraints(maxWidth: 200),
child: Text( child: Text(displayname, maxLines: 1, overflow: .ellipsis),
displayname,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
), ),
], ],
), ),
), ),
const PopupMenuDivider(),
if (space != null) if (space != null)
PopupMenuItem( PopupMenuItem(
value: ChatContextAction.goToSpace, value: ChatContextAction.goToSpace,

View file

@ -1,3 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -5,10 +6,11 @@ import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/presence_builder.dart'; import 'package:fluffychat/widgets/presence_builder.dart';
import '../../utils/url_launcher.dart'; import '../../utils/url_launcher.dart';
@ -37,7 +39,6 @@ class UserDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
final dmRoomId = client.getDirectChatFromUserId(profile.userId);
final displayname = final displayname =
profile.displayName ?? profile.displayName ??
profile.userId.localpart ?? profile.userId.localpart ??
@ -46,12 +47,8 @@ class UserDialog extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final avatar = profile.avatarUrl; final avatar = profile.avatarUrl;
return AlertDialog.adaptive( return AlertDialog.adaptive(
title: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 256),
child: Center(child: Text(displayname, textAlign: TextAlign.center)),
),
content: ConstrainedBox( content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256), constraints: const BoxConstraints(maxWidth: 256),
child: PresenceBuilder( child: PresenceBuilder(
userId: profile.userId, userId: profile.userId,
client: Matrix.of(context).client, client: Matrix.of(context).client,
@ -66,17 +63,18 @@ class UserDialog extends StatelessWidget {
lastActiveTimestamp.localizedTimeShort(context), lastActiveTimestamp.localizedTimeShort(context),
) )
: null; : null;
return SingleChildScrollView( return Column(
child: Column( spacing: 16,
spacing: 8, mainAxisSize: .min,
mainAxisSize: .min, crossAxisAlignment: .stretch,
crossAxisAlignment: .stretch, children: [
children: [ Row(
Center( spacing: 12,
child: Avatar( children: [
Avatar(
mxContent: avatar, mxContent: avatar,
name: displayname, name: displayname,
size: Avatar.defaultSize * 2, size: Avatar.defaultSize * 1.5,
onTap: avatar != null onTap: avatar != null
? () => showDialog( ? () => showDialog(
context: context, context: context,
@ -84,130 +82,178 @@ class UserDialog extends StatelessWidget {
) )
: null, : null,
), ),
), Expanded(
HoverBuilder( child: Column(
builder: (context, hovered) => StatefulBuilder( crossAxisAlignment: .start,
builder: (context, setState) => MouseRegion( children: [
cursor: SystemMouseCursors.click, Text(
child: GestureDetector( displayname,
onTap: () { maxLines: 1,
Clipboard.setData( overflow: .ellipsis,
ClipboardData(text: profile.userId), style: TextStyle(fontSize: 16),
); ),
setState(() { const SizedBox(height: 8),
copied = true; HoverBuilder(
}); builder: (context, hovered) => StatefulBuilder(
}, builder: (context, setState) => MouseRegion(
child: RichText( cursor: SystemMouseCursors.click,
text: TextSpan( child: GestureDetector(
children: [ onTap: () {
WidgetSpan( Clipboard.setData(
child: Padding( ClipboardData(text: profile.userId),
padding: const EdgeInsets.only(right: 4.0), );
child: AnimatedScale( setState(() {
duration: FluffyThemes.animationDuration, copied = true;
curve: FluffyThemes.animationCurve, });
scale: hovered },
? 1.33 child: RichText(
: copied text: TextSpan(
? 1.25 children: [
: 1.0, WidgetSpan(
child: Icon( child: Padding(
copied padding: const EdgeInsets.only(
? Icons.check_circle right: 4.0,
: Icons.copy, ),
size: 12, child: AnimatedScale(
color: copied ? Colors.green : null, duration: FluffyThemes
), .animationDuration,
curve:
FluffyThemes.animationCurve,
scale: hovered
? 1.33
: copied
? 1.25
: 1.0,
child: Icon(
copied
? Icons.check_circle
: Icons.copy,
size: 12,
color: copied
? Colors.green
: null,
),
),
),
),
TextSpan(text: profile.userId),
],
style: theme.textTheme.bodyMedium
?.copyWith(fontSize: 10),
), ),
maxLines: 1,
overflow: .ellipsis,
textAlign: TextAlign.center,
), ),
), ),
TextSpan(text: profile.userId), ),
], ),
),
if (presenceText != null)
Text(
presenceText,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 10, fontSize: 10,
), ),
), ),
textAlign: TextAlign.center, ],
),
),
), ),
), ),
],
),
if (statusMsg != null)
SelectableLinkify(
text: statusMsg,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
textAlign: TextAlign.start,
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: theme.colorScheme.primary,
decoration: TextDecoration.underline,
decorationColor: theme.colorScheme.primary,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
), ),
if (presenceText != null) Row(
Text( mainAxisAlignment: .spaceBetween,
presenceText, spacing: 4,
style: const TextStyle(fontSize: 10), children: [
textAlign: TextAlign.center, _IconTextButton(
label: L10n.of(context).chat,
icon: Icons.chat_outlined,
onTap: () async {
final router = GoRouter.of(context);
final roomIdResult = await showFutureLoadingDialog(
context: context,
future: () => client.startDirectChat(profile.userId),
);
final roomId = roomIdResult.result;
if (roomId == null) return;
if (context.mounted) Navigator.of(context).pop();
router.go('/rooms/$roomId');
},
), ),
if (statusMsg != null) _IconTextButton(
SelectableLinkify( label: L10n.of(context).block,
text: statusMsg, icon: Icons.block_outlined,
textScaleFactor: MediaQuery.textScalerOf( onTap: () {
context, final router = GoRouter.of(context);
).scale(1), Navigator.of(context).pop();
textAlign: TextAlign.center, router.go(
options: const LinkifyOptions(humanize: false), '/rooms/settings/security/ignorelist',
linkStyle: TextStyle( extra: profile.userId,
color: theme.colorScheme.primary, );
decoration: TextDecoration.underline, },
decorationColor: theme.colorScheme.primary,
),
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
), ),
], _IconTextButton(
), label: L10n.of(context).share,
icon: Icons.adaptive.share,
onTap: () => FluffyShare.share(profile.userId, context),
),
],
),
],
); );
}, },
), ),
), ),
actions: [ );
if (client.userID != profile.userId) ...[ }
AdaptiveDialogAction( }
borderRadius: AdaptiveDialogAction.topRadius,
bigButtons: true, class _IconTextButton extends StatelessWidget {
onPressed: () async { final String label;
final router = GoRouter.of(context); final IconData icon;
final roomIdResult = await showFutureLoadingDialog( final VoidCallback onTap;
context: context, const _IconTextButton({
future: () => client.startDirectChat(profile.userId), required this.label,
); required this.icon,
final roomId = roomIdResult.result; required this.onTap,
if (roomId == null) return; });
if (context.mounted) Navigator.of(context).pop();
router.go('/rooms/$roomId'); @override
}, Widget build(BuildContext context) {
child: Text( final theme = Theme.of(context);
dmRoomId == null return Expanded(
? L10n.of(context).startConversation child: CupertinoButton(
: L10n.of(context).sendAMessage, onPressed: onTap,
), borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
), color: theme.colorScheme.surfaceBright,
AdaptiveDialogAction( padding: EdgeInsets.all(8),
bigButtons: true, child: Column(
borderRadius: AdaptiveDialogAction.centerRadius, mainAxisSize: .min,
onPressed: () { children: [
final router = GoRouter.of(context); Icon(icon),
Navigator.of(context).pop(); Text(
router.go( label,
'/rooms/settings/security/ignorelist', style: TextStyle(fontSize: 12),
extra: profile.userId, maxLines: 1,
); overflow: .ellipsis,
}, ),
child: Text( ],
L10n.of(context).ignoreUser, ),
style: TextStyle(color: theme.colorScheme.error), ),
),
),
],
AdaptiveDialogAction(
bigButtons: true,
borderRadius: AdaptiveDialogAction.bottomRadius,
onPressed: Navigator.of(context).pop,
child: Text(L10n.of(context).close),
),
],
); );
} }
} }

View file

@ -45,40 +45,22 @@ Future<void> showMemberActionsPopupMenu({
children: [ children: [
Avatar( Avatar(
name: displayname, name: displayname,
size: 30,
mxContent: user.avatarUrl, mxContent: user.avatarUrl,
presenceUserId: user.id, presenceUserId: user.id,
presenceBackgroundColor: theme.colorScheme.surfaceContainer, presenceBackgroundColor: theme.colorScheme.surfaceContainer,
), ),
Column( ConstrainedBox(
mainAxisSize: .min, constraints: const BoxConstraints(maxWidth: 200),
crossAxisAlignment: .start, child: Text(
children: [ displayname,
ConstrainedBox( maxLines: 1,
constraints: const BoxConstraints(maxWidth: 128), overflow: TextOverflow.ellipsis,
child: Text( ),
displayname,
textAlign: TextAlign.center,
style: theme.textTheme.labelLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 128),
child: Text(
user.id,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 10),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
), ),
], ],
), ),
), ),
const PopupMenuDivider(),
if (onMention != null) if (onMention != null)
PopupMenuItem( PopupMenuItem(
value: _MemberActions.mention, value: _MemberActions.mention,