forked from Nexus/nexus
redesign user popover
This commit is contained in:
parent
e15d947fac
commit
d46646d781
9 changed files with 302 additions and 272 deletions
|
|
@ -1,25 +1,18 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:nexus/helpers/extensions/show_context_menu.dart";
|
||||
import "package:nexus/models/content/membership.dart";
|
||||
import "package:nexus/widgets/user_popover.dart";
|
||||
import "package:nexus/widgets/user_bottom_sheet.dart";
|
||||
|
||||
extension ShowUserPopover on BuildContext {
|
||||
void showUserPopover(
|
||||
MembershipContent member,
|
||||
String userId, {
|
||||
String? roomId,
|
||||
required Offset globalPosition,
|
||||
}) => showContextMenu(
|
||||
globalPosition: globalPosition,
|
||||
children: [
|
||||
PopupMenuItem(
|
||||
enabled: false,
|
||||
padding: .symmetric(horizontal: 16, vertical: 8),
|
||||
child: IconTheme(
|
||||
data: .new(),
|
||||
child: UserPopover(member, userId, roomId: roomId),
|
||||
}) => showModalBottomSheet(
|
||||
constraints: BoxConstraints.loose(
|
||||
Size(500, View.of(this).physicalSize.height - 80),
|
||||
),
|
||||
),
|
||||
],
|
||||
isScrollControlled: true,
|
||||
context: this,
|
||||
builder: (context) => UserBottomSheet(member, userId, roomId: roomId),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class AvatarOrHash extends ConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final box = ColoredBox(
|
||||
color: ColorHash(title).color,
|
||||
child: Center(child: Text(title.isEmpty ? "" : title[0])),
|
||||
child: Center(child: Icon(Icons.person, size: height / 2)),
|
||||
);
|
||||
|
||||
final parsedAvatar = avatar?.mxcToHttps(
|
||||
|
|
|
|||
|
|
@ -25,12 +25,7 @@ class MentionChip extends ConsumerWidget {
|
|||
: InkWell(
|
||||
onTapUp: (details) {
|
||||
if (membership != null) {
|
||||
context.showUserPopover(
|
||||
membership,
|
||||
mention,
|
||||
roomId: roomId,
|
||||
globalPosition: details.globalPosition,
|
||||
);
|
||||
context.showUserPopover(membership, mention, roomId: roomId);
|
||||
}
|
||||
},
|
||||
child: IgnorePointer(
|
||||
|
|
|
|||
|
|
@ -12,15 +12,12 @@ class MessageAvatar extends ConsumerWidget {
|
|||
const MessageAvatar(this.event, {this.height = 24, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) =>
|
||||
switch (ref.watch(AuthorController.provider(event))) {
|
||||
Widget build(BuildContext context, WidgetRef ref) => switch (ref.watch(
|
||||
AuthorController.provider(event),
|
||||
)) {
|
||||
AsyncData(:final value) || AsyncLoading(:final value?) => InkWell(
|
||||
onTapUp: (details) => context.showUserPopover(
|
||||
value,
|
||||
event.sender,
|
||||
roomId: event.roomId,
|
||||
globalPosition: details.globalPosition,
|
||||
),
|
||||
onTapUp: (details) =>
|
||||
context.showUserPopover(value, event.sender, roomId: event.roomId),
|
||||
child: AvatarOrHash(
|
||||
value.avatarUrl,
|
||||
value.displayName ?? event.sender.localpart,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ class MessageDisplayname extends ConsumerWidget {
|
|||
value,
|
||||
event.sender,
|
||||
roomId: event.roomId,
|
||||
globalPosition: details.globalPosition,
|
||||
)
|
||||
: null,
|
||||
child: Wrap(
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import "package:nexus/widgets/avatar_or_hash.dart";
|
|||
import "package:nexus/widgets/divider_text.dart";
|
||||
import "package:nexus/widgets/error_dialog.dart";
|
||||
import "package:nexus/widgets/loading.dart";
|
||||
import "package:nexus/widgets/user_bottom_sheet.dart";
|
||||
|
||||
class MemberList extends HookConsumerWidget {
|
||||
final String roomId;
|
||||
|
|
@ -138,8 +139,11 @@ class MemberList extends HookConsumerWidget {
|
|||
),
|
||||
leading: AvatarOrHash(
|
||||
avatarUrl,
|
||||
height: 36,
|
||||
displayName ??
|
||||
members[index].sender.localpart,
|
||||
members[index]
|
||||
.stateKey!
|
||||
.localpart,
|
||||
),
|
||||
),
|
||||
_ => throw Exception(
|
||||
|
|
@ -147,12 +151,25 @@ class MemberList extends HookConsumerWidget {
|
|||
),
|
||||
},
|
||||
onTap: (index) {
|
||||
// context.showUserPopover(
|
||||
// member.content as MembershipContent,
|
||||
// member.stateKey!,
|
||||
// roomId: roomId,
|
||||
// globalPosition: details.globalPosition,
|
||||
// ),
|
||||
final member = members[index];
|
||||
if (member.content
|
||||
case MembershipContent content) {
|
||||
showModalBottomSheet(
|
||||
constraints: BoxConstraints.loose(
|
||||
Size(
|
||||
500,
|
||||
(context.size?.height ?? 1000) - 80,
|
||||
),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => UserBottomSheet(
|
||||
content,
|
||||
member.stateKey!,
|
||||
roomId: roomId,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ class MembershipRenderer extends StatelessWidget {
|
|||
content,
|
||||
event.stateKey!,
|
||||
roomId: event.roomId,
|
||||
globalPosition: details.globalPosition,
|
||||
),
|
||||
child: Text(
|
||||
overflow: .ellipsis,
|
||||
|
|
|
|||
254
lib/widgets/user_bottom_sheet.dart
Normal file
254
lib/widgets/user_bottom_sheet.dart
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
import "package:collection/collection.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:intl/intl.dart";
|
||||
import "package:m3e_buttons/m3e_buttons.dart";
|
||||
import "package:nexus/controllers/client_controller.dart";
|
||||
import "package:nexus/controllers/client_state_controller.dart";
|
||||
import "package:nexus/controllers/power_level_controller.dart";
|
||||
import "package:nexus/controllers/profile_controller.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
||||
import "package:nexus/models/content/membership.dart";
|
||||
import "package:nexus/models/requests/membership_action.dart";
|
||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||
import "package:nexus/main.dart";
|
||||
import "package:nexus/widgets/expandable_image.dart";
|
||||
|
||||
class UserBottomSheet extends ConsumerWidget {
|
||||
final MembershipContent member;
|
||||
final String userId;
|
||||
final String? roomId;
|
||||
const UserBottomSheet(this.member, this.userId, {this.roomId, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final client = ref.watch(ClientController.provider.notifier);
|
||||
|
||||
void showMembershipDialog(MembershipAction action) => showDialog(
|
||||
context: context,
|
||||
builder: (context) => HookBuilder(
|
||||
builder: (context) {
|
||||
final actionReasonController = useTextEditingController();
|
||||
return AlertDialog(
|
||||
title: Text("${toBeginningOfSentenceCase(action.name)} $userId"),
|
||||
content: Column(
|
||||
mainAxisSize: .min,
|
||||
crossAxisAlignment: .start,
|
||||
children: [
|
||||
Text("Are you sure you want to ${action.name} $userId?"),
|
||||
SizedBox(height: 12),
|
||||
TextField(
|
||||
textCapitalization: .sentences,
|
||||
controller: actionReasonController,
|
||||
decoration: .new(
|
||||
labelText: "Reason for ${action.name} (optional)",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: Text("Cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
client
|
||||
.setMembership(
|
||||
.new(
|
||||
userId: userId,
|
||||
roomId: roomId!,
|
||||
action: action,
|
||||
reason: actionReasonController.text,
|
||||
),
|
||||
)
|
||||
.onError(showError);
|
||||
},
|
||||
child: Text(toBeginningOfSentenceCase(action.name)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: .all(42),
|
||||
child: Column(
|
||||
spacing: 4,
|
||||
mainAxisSize: .min,
|
||||
crossAxisAlignment: .center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: .end,
|
||||
children: [
|
||||
M3EButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 18),
|
||||
|
||||
ExpandableImage(
|
||||
member.avatarUrl
|
||||
?.mxcToHttps(
|
||||
ref.watch(
|
||||
ClientStateController.provider.select(
|
||||
(value) => value!.homeserverUrl!,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toString(),
|
||||
child: AvatarOrHash(
|
||||
member.avatarUrl,
|
||||
member.displayName ?? userId.localpart,
|
||||
height: 200,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 8),
|
||||
|
||||
SelectableText(
|
||||
member.displayName ?? userId.localpart,
|
||||
style: textTheme.headlineLarge,
|
||||
),
|
||||
SelectableText(
|
||||
userId,
|
||||
style: textTheme.titleSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
|
||||
ref
|
||||
.watch(ProfileController.provider(userId))
|
||||
.betterWhen(
|
||||
loading: SizedBox.shrink,
|
||||
data: (profile) => Column(
|
||||
children: [
|
||||
Wrap(
|
||||
crossAxisAlignment: .center,
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
...profile.pronouns
|
||||
.where((pronoun) => pronoun.language == "en")
|
||||
.mapIndexed(
|
||||
(index, pronoun) => [
|
||||
if (index != 0)
|
||||
Icon(
|
||||
Icons.circle,
|
||||
size: 4,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
Text(
|
||||
pronoun.summary,
|
||||
style: textTheme.titleSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
.flattened,
|
||||
|
||||
if (profile.timezone != null) ...[
|
||||
if (profile.pronouns.isNotEmpty)
|
||||
SizedBox(
|
||||
height: 16,
|
||||
child: VerticalDivider(
|
||||
thickness: 1.5,
|
||||
width: 4,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
profile.timezone!,
|
||||
style: textTheme.titleSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 8),
|
||||
if (userId != ref.watch(ClientStateController.provider)?.userId &&
|
||||
roomId != null) ...[
|
||||
if (ref.watch(
|
||||
PowerLevelController.provider(
|
||||
.membershipAction(
|
||||
action: .kick,
|
||||
roomId: roomId!,
|
||||
targetUser: userId,
|
||||
),
|
||||
),
|
||||
) &&
|
||||
member.status == .join ||
|
||||
member.status == .invite)
|
||||
Padding(
|
||||
padding: .only(bottom: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
spacing: 8,
|
||||
children: [
|
||||
M3EButton.icon(
|
||||
onPressed: () => showMembershipDialog(.kick),
|
||||
shape: .square,
|
||||
icon: Icon(Icons.sports_martial_arts),
|
||||
label: Text("Kick"),
|
||||
decoration: .new(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
theme.colorScheme.error,
|
||||
),
|
||||
foregroundColor: WidgetStatePropertyAll(
|
||||
theme.colorScheme.onError,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
M3EButton.icon(
|
||||
onPressed: () => showMembershipDialog(.ban),
|
||||
shape: .square,
|
||||
icon: Icon(Icons.gavel),
|
||||
label: Text("Ban"),
|
||||
decoration: .new(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
theme.colorScheme.errorContainer,
|
||||
),
|
||||
foregroundColor: WidgetStatePropertyAll(
|
||||
theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
].map((e) => Expanded(child: e)).toList(),
|
||||
),
|
||||
),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: M3EButton.icon(
|
||||
onPressed: null,
|
||||
shape: .square,
|
||||
style: .tonal,
|
||||
icon: Icon(Icons.message),
|
||||
label: Text("Message"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:intl/intl.dart";
|
||||
import "package:nexus/controllers/client_controller.dart";
|
||||
import "package:nexus/controllers/client_state_controller.dart";
|
||||
import "package:nexus/controllers/power_level_controller.dart";
|
||||
import "package:nexus/controllers/profile_controller.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||
import "package:nexus/helpers/extensions/mxc_to_https.dart";
|
||||
import "package:nexus/models/content/membership.dart";
|
||||
import "package:nexus/models/requests/membership_action.dart";
|
||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||
import "package:nexus/main.dart";
|
||||
import "package:nexus/widgets/expandable_image.dart";
|
||||
|
||||
class UserPopover extends ConsumerWidget {
|
||||
final MembershipContent member;
|
||||
final String userId;
|
||||
final String? roomId;
|
||||
const UserPopover(this.member, this.userId, {this.roomId, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final client = ref.watch(ClientController.provider.notifier);
|
||||
|
||||
void showMembershipDialog(MembershipAction action) => showDialog(
|
||||
context: context,
|
||||
builder: (context) => HookBuilder(
|
||||
builder: (context) {
|
||||
final actionReasonController = useTextEditingController();
|
||||
return AlertDialog(
|
||||
title: Text("${toBeginningOfSentenceCase(action.name)} $userId"),
|
||||
content: Column(
|
||||
mainAxisSize: .min,
|
||||
crossAxisAlignment: .start,
|
||||
children: [
|
||||
Text("Are you sure you want to ${action.name} $userId?"),
|
||||
SizedBox(height: 12),
|
||||
TextField(
|
||||
textCapitalization: .sentences,
|
||||
controller: actionReasonController,
|
||||
decoration: .new(
|
||||
labelText: "Reason for ${action.name} (optional)",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: Text("Cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
client
|
||||
.setMembership(
|
||||
.new(
|
||||
userId: userId,
|
||||
roomId: roomId!,
|
||||
action: action,
|
||||
reason: actionReasonController.text,
|
||||
),
|
||||
)
|
||||
.onError(showError);
|
||||
},
|
||||
child: Text(toBeginningOfSentenceCase(action.name)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final actionButton = ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(.symmetric(horizontal: 24, vertical: 18)),
|
||||
shape: WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(borderRadius: .circular(8)),
|
||||
),
|
||||
);
|
||||
|
||||
return Column(
|
||||
spacing: 16,
|
||||
crossAxisAlignment: .stretch,
|
||||
children: [
|
||||
Wrap(
|
||||
alignment: .center,
|
||||
spacing: 16,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
ExpandableImage(
|
||||
member.avatarUrl
|
||||
?.mxcToHttps(
|
||||
ref.watch(
|
||||
ClientStateController.provider.select(
|
||||
(value) => value!.homeserverUrl!,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toString(),
|
||||
child: AvatarOrHash(
|
||||
member.avatarUrl,
|
||||
member.displayName ?? userId.localpart,
|
||||
height: 80,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
SelectableText(
|
||||
member.displayName ?? userId.localpart,
|
||||
style: textTheme.headlineSmall,
|
||||
),
|
||||
SelectableText(userId, style: textTheme.titleSmall),
|
||||
SizedBox(height: 4),
|
||||
ref
|
||||
.watch(ProfileController.provider(userId))
|
||||
.betterWhen(
|
||||
loading: SizedBox.shrink,
|
||||
data: (profile) => Wrap(
|
||||
alignment: .center,
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
for (final pronoun in profile.pronouns.where(
|
||||
(pronoun) => pronoun.language == "en",
|
||||
))
|
||||
Chip(
|
||||
label: Text(pronoun.summary),
|
||||
labelStyle: .new(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
color: WidgetStatePropertyAll(
|
||||
theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
if (profile.timezone != null)
|
||||
Chip(
|
||||
label: Text(profile.timezone!),
|
||||
labelStyle: .new(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
color: WidgetStatePropertyAll(
|
||||
theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (userId != ref.watch(ClientStateController.provider)?.userId &&
|
||||
roomId != null)
|
||||
Wrap(
|
||||
alignment: .center,
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
onPressed: null,
|
||||
label: Text("Message"),
|
||||
icon: Icon(Icons.message),
|
||||
style: actionButton,
|
||||
),
|
||||
|
||||
if (ref.watch(
|
||||
PowerLevelController.provider(
|
||||
.membershipAction(
|
||||
action: .kick,
|
||||
roomId: roomId!,
|
||||
targetUser: userId,
|
||||
),
|
||||
),
|
||||
) &&
|
||||
member.status == .join ||
|
||||
member.status == .invite)
|
||||
FilledButton.icon(
|
||||
onPressed: () => showMembershipDialog(.kick),
|
||||
label: Text("Kick"),
|
||||
style: actionButton.copyWith(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
theme.colorScheme.error,
|
||||
),
|
||||
foregroundColor: WidgetStatePropertyAll(
|
||||
theme.colorScheme.onError,
|
||||
),
|
||||
),
|
||||
icon: Icon(Icons.sports_martial_arts),
|
||||
),
|
||||
if (ref.watch(
|
||||
PowerLevelController.provider(
|
||||
.membershipAction(
|
||||
roomId: roomId!,
|
||||
action: .ban,
|
||||
targetUser: userId,
|
||||
),
|
||||
),
|
||||
))
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => showMembershipDialog(
|
||||
member.status == .ban ? .unban : .ban,
|
||||
),
|
||||
icon: Icon(Icons.gavel),
|
||||
label: Text(member.status == .ban ? "Unban" : "Ban"),
|
||||
style: actionButton.copyWith(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
theme.colorScheme.errorContainer,
|
||||
),
|
||||
foregroundColor: WidgetStatePropertyAll(
|
||||
theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue