forked from Nexus/nexus
Compare commits
11 commits
309d0df581
...
ddc8db8326
| Author | SHA1 | Date | |
|---|---|---|---|
|
ddc8db8326 |
|||
|
bc307cbcda |
|||
|
e40de37b9d |
|||
|
7757825e27 |
|||
|
795e9b6e9b |
|||
|
abba60c28b |
|||
|
8cea4b6cf3 |
|||
|
d46646d781 |
|||
|
e15d947fac |
|||
|
562ce5d4ff |
|||
|
f60189bfb8 |
14 changed files with 380 additions and 300 deletions
|
|
@ -113,7 +113,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
||||||
- [ ] Notifications using UnifiedPush ([#35](https://git.federated.nexus/Nexus/nexus/issues/35))
|
- [ ] Notifications using UnifiedPush ([#35](https://git.federated.nexus/Nexus/nexus/issues/35))
|
||||||
- [ ] Group calls using [MSC4195](https://github.com/matrix-org/matrix-spec-proposals/pull/4195)
|
- [ ] Group calls using [MSC4195](https://github.com/matrix-org/matrix-spec-proposals/pull/4195)
|
||||||
- [ ] Invites
|
- [ ] Invites
|
||||||
- [ ] Settings
|
- [ ] Settings ([#37](https://git.federated.nexus/Nexus/nexus/issues/37))
|
||||||
- [ ] Matrix: URIs vs Matrix.to links
|
- [ ] Matrix: URIs vs Matrix.to links
|
||||||
- [ ] Light/Dark mode
|
- [ ] Light/Dark mode
|
||||||
- [ ] Remote Gomuks instance
|
- [ ] Remote Gomuks instance
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,18 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:nexus/helpers/extensions/show_context_menu.dart";
|
|
||||||
import "package:nexus/models/content/membership.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 {
|
extension ShowUserPopover on BuildContext {
|
||||||
void showUserPopover(
|
void showUserPopover(
|
||||||
MembershipContent member,
|
MembershipContent member,
|
||||||
String userId, {
|
String userId, {
|
||||||
String? roomId,
|
String? roomId,
|
||||||
required Offset globalPosition,
|
}) => showModalBottomSheet(
|
||||||
}) => showContextMenu(
|
constraints: BoxConstraints.loose(
|
||||||
globalPosition: globalPosition,
|
Size(500, View.of(this).physicalSize.height - 80),
|
||||||
children: [
|
|
||||||
PopupMenuItem(
|
|
||||||
enabled: false,
|
|
||||||
padding: .symmetric(horizontal: 16, vertical: 8),
|
|
||||||
child: IconTheme(
|
|
||||||
data: .new(),
|
|
||||||
child: UserPopover(member, userId, roomId: roomId),
|
|
||||||
),
|
),
|
||||||
),
|
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) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final box = ColoredBox(
|
final box = ColoredBox(
|
||||||
color: ColorHash(title).color,
|
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(
|
final parsedAvatar = avatar?.mxcToHttps(
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
|
|
||||||
class FlashWrapper extends StatelessWidget {
|
class HighlightWrapper extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final bool isFlashing;
|
final bool isHighlighted;
|
||||||
const FlashWrapper(this.child, {this.isFlashing = false, super.key});
|
const HighlightWrapper(this.child, {this.isHighlighted = false, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => ClipRRect(
|
Widget build(BuildContext context) => ClipRRect(
|
||||||
borderRadius: .all(.circular(12)),
|
borderRadius: .all(.circular(12)),
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
padding: isFlashing ? .all(8) : .all(0),
|
padding: isHighlighted ? .all(8) : .all(0),
|
||||||
color: isFlashing
|
color: isHighlighted
|
||||||
? Theme.of(context).colorScheme.onSurface.withAlpha(50)
|
? Theme.of(context).colorScheme.onSurface.withAlpha(50)
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
duration: .new(milliseconds: 250),
|
duration: .new(milliseconds: 250),
|
||||||
|
|
@ -23,14 +23,9 @@ class MentionChip extends ConsumerWidget {
|
||||||
return mention == null
|
return mention == null
|
||||||
? SizedBox.shrink()
|
? SizedBox.shrink()
|
||||||
: InkWell(
|
: InkWell(
|
||||||
onTapUp: (details) {
|
onTap: () {
|
||||||
if (membership != null) {
|
if (membership != null) {
|
||||||
context.showUserPopover(
|
context.showUserPopover(membership, mention, roomId: roomId);
|
||||||
membership,
|
|
||||||
mention,
|
|
||||||
roomId: roomId,
|
|
||||||
globalPosition: details.globalPosition,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,12 @@ class MessageAvatar extends ConsumerWidget {
|
||||||
const MessageAvatar(this.event, {this.height = 24, super.key});
|
const MessageAvatar(this.event, {this.height = 24, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) =>
|
Widget build(BuildContext context, WidgetRef ref) => switch (ref.watch(
|
||||||
switch (ref.watch(AuthorController.provider(event))) {
|
AuthorController.provider(event),
|
||||||
|
)) {
|
||||||
AsyncData(:final value) || AsyncLoading(:final value?) => InkWell(
|
AsyncData(:final value) || AsyncLoading(:final value?) => InkWell(
|
||||||
onTapUp: (details) => context.showUserPopover(
|
onTap: () =>
|
||||||
value,
|
context.showUserPopover(value, event.sender, roomId: event.roomId),
|
||||||
event.sender,
|
|
||||||
roomId: event.roomId,
|
|
||||||
globalPosition: details.globalPosition,
|
|
||||||
),
|
|
||||||
child: AvatarOrHash(
|
child: AvatarOrHash(
|
||||||
value.avatarUrl,
|
value.avatarUrl,
|
||||||
value.displayName ?? event.sender.localpart,
|
value.displayName ?? event.sender.localpart,
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,11 @@ class MessageDisplayname extends ConsumerWidget {
|
||||||
AuthorController.provider(event),
|
AuthorController.provider(event),
|
||||||
)) {
|
)) {
|
||||||
AsyncData(:final value) || AsyncLoading(:final value?) => InkWell(
|
AsyncData(:final value) || AsyncLoading(:final value?) => InkWell(
|
||||||
onTapUp: clickable
|
onTap: clickable
|
||||||
? (details) => context.showUserPopover(
|
? () => context.showUserPopover(
|
||||||
value,
|
value,
|
||||||
event.sender,
|
event.sender,
|
||||||
roomId: event.roomId,
|
roomId: event.roomId,
|
||||||
globalPosition: details.globalPosition,
|
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
child: Wrap(
|
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/divider_text.dart";
|
||||||
import "package:nexus/widgets/error_dialog.dart";
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
import "package:nexus/widgets/loading.dart";
|
import "package:nexus/widgets/loading.dart";
|
||||||
|
import "package:nexus/widgets/user_bottom_sheet.dart";
|
||||||
|
|
||||||
class MemberList extends HookConsumerWidget {
|
class MemberList extends HookConsumerWidget {
|
||||||
final String roomId;
|
final String roomId;
|
||||||
|
|
@ -138,8 +139,11 @@ class MemberList extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
leading: AvatarOrHash(
|
leading: AvatarOrHash(
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
|
height: 36,
|
||||||
displayName ??
|
displayName ??
|
||||||
members[index].sender.localpart,
|
members[index]
|
||||||
|
.stateKey!
|
||||||
|
.localpart,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_ => throw Exception(
|
_ => throw Exception(
|
||||||
|
|
@ -147,12 +151,25 @@ class MemberList extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
onTap: (index) {
|
onTap: (index) {
|
||||||
// context.showUserPopover(
|
final member = members[index];
|
||||||
// member.content as MembershipContent,
|
if (member.content
|
||||||
// member.stateKey!,
|
case MembershipContent content) {
|
||||||
// roomId: roomId,
|
showModalBottomSheet(
|
||||||
// globalPosition: details.globalPosition,
|
constraints: BoxConstraints.loose(
|
||||||
// ),
|
Size(
|
||||||
|
500,
|
||||||
|
(context.size?.height ?? 1000) - 80,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isScrollControlled: true,
|
||||||
|
context: context,
|
||||||
|
builder: (context) => UserBottomSheet(
|
||||||
|
content,
|
||||||
|
member.stateKey!,
|
||||||
|
roomId: roomId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:nexus/helpers/extensions/show_context_menu.dart";
|
import "package:nexus/helpers/extensions/show_context_menu.dart";
|
||||||
import "package:nexus/models/content/avatar.dart";
|
import "package:nexus/models/content/avatar.dart";
|
||||||
import "package:nexus/models/content/canonical_alias.dart";
|
import "package:nexus/models/content/canonical_alias.dart";
|
||||||
|
|
@ -24,7 +25,7 @@ import "package:nexus/widgets/reaction_row.dart";
|
||||||
import "package:nexus/widgets/renderers/membership.dart";
|
import "package:nexus/widgets/renderers/membership.dart";
|
||||||
import "package:nexus/widgets/renderers/generic_event.dart";
|
import "package:nexus/widgets/renderers/generic_event.dart";
|
||||||
|
|
||||||
class EventRenderer extends ConsumerWidget {
|
class EventRenderer extends HookConsumerWidget {
|
||||||
final Event event;
|
final Event event;
|
||||||
final bool textOnly;
|
final bool textOnly;
|
||||||
final bool isGrouped;
|
final bool isGrouped;
|
||||||
|
|
@ -46,6 +47,8 @@ class EventRenderer extends ConsumerWidget {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final colorScheme = theme.colorScheme;
|
final colorScheme = theme.colorScheme;
|
||||||
final errorStyle = TextStyle(color: colorScheme.error);
|
final errorStyle = TextStyle(color: colorScheme.error);
|
||||||
|
final focusNode = useFocusNode();
|
||||||
|
useListenable(focusNode);
|
||||||
|
|
||||||
final child = event.redactedBy != null || event.relationType == "m.replace"
|
final child = event.redactedBy != null || event.relationType == "m.replace"
|
||||||
? null
|
? null
|
||||||
|
|
@ -168,14 +171,46 @@ class EventRenderer extends ConsumerWidget {
|
||||||
if (textOnly)
|
if (textOnly)
|
||||||
child
|
child
|
||||||
else ...[
|
else ...[
|
||||||
GestureDetector(
|
Builder(
|
||||||
|
builder: (context) => FocusableActionDetector(
|
||||||
|
focusNode: focusNode,
|
||||||
|
actions: contextMenuCallback == null
|
||||||
|
? null
|
||||||
|
: {
|
||||||
|
ActivateIntent: CallbackAction(
|
||||||
|
onInvoke: (_) {
|
||||||
|
final renderBox =
|
||||||
|
context.findRenderObject() as RenderBox;
|
||||||
|
final topLeft = renderBox.localToGlobal(
|
||||||
|
Offset.zero,
|
||||||
|
);
|
||||||
|
context.showContextMenu(
|
||||||
|
globalPosition: topLeft,
|
||||||
|
children: getEventOptions!(event).toList(),
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: focusNode.hasPrimaryFocus
|
||||||
|
? theme.colorScheme.surfaceContainerHighest
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: GestureDetector(
|
||||||
onSecondaryTapUp: contextMenuCallback,
|
onSecondaryTapUp: contextMenuCallback,
|
||||||
onLongPressStart: contextMenuCallback,
|
onLongPressStart: contextMenuCallback,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: isGrouped ? .zero : .only(top: 8),
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
).copyWith(top: isGrouped ? 0 : 8),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
...[
|
...[
|
||||||
if (event.content is! MessageContent) ReactionRow(event),
|
if (event.content is! MessageContent) ReactionRow(event),
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,10 @@ class MembershipRenderer extends StatelessWidget {
|
||||||
return switch (event.content) {
|
return switch (event.content) {
|
||||||
MembershipContent content => GenericEventRenderer(Icons.people, [
|
MembershipContent content => GenericEventRenderer(Icons.people, [
|
||||||
InkWell(
|
InkWell(
|
||||||
onTapUp: (details) => context.showUserPopover(
|
onTap: () => context.showUserPopover(
|
||||||
content,
|
content,
|
||||||
event.stateKey!,
|
event.stateKey!,
|
||||||
roomId: event.roomId,
|
roomId: event.roomId,
|
||||||
globalPosition: details.globalPosition,
|
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
overflow: .ellipsis,
|
overflow: .ellipsis,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import "package:nexus/widgets/emoji_picker_button.dart";
|
||||||
import "package:nexus/widgets/renderers/event.dart";
|
import "package:nexus/widgets/renderers/event.dart";
|
||||||
import "package:nexus/widgets/member_list.dart";
|
import "package:nexus/widgets/member_list.dart";
|
||||||
import "package:nexus/widgets/room_appbar.dart";
|
import "package:nexus/widgets/room_appbar.dart";
|
||||||
import "package:nexus/widgets/flash_wrapper.dart";
|
import "package:nexus/widgets/highlight_wrapper.dart";
|
||||||
import "package:nexus/widgets/error_dialog.dart";
|
import "package:nexus/widgets/error_dialog.dart";
|
||||||
import "package:nexus/main.dart";
|
import "package:nexus/main.dart";
|
||||||
import "package:nexus/widgets/loading.dart";
|
import "package:nexus/widgets/loading.dart";
|
||||||
|
|
@ -41,7 +41,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final relatedEvent = useState<Event?>(null);
|
final relatedEvent = useState<Event?>(null);
|
||||||
final relationType = useState(RelationType.reply);
|
final relationType = useState(RelationType.reply);
|
||||||
final flashingEvent = useState<String?>(null);
|
final highlightedEvent = useState<String?>(null);
|
||||||
|
|
||||||
final composerSize = useState<double>(64);
|
final composerSize = useState<double>(64);
|
||||||
|
|
||||||
|
|
@ -400,7 +400,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: .symmetric(horizontal: 12),
|
padding: .symmetric(horizontal: 4),
|
||||||
child: switch (controllerData) {
|
child: switch (controllerData) {
|
||||||
AsyncData(:final value?) ||
|
AsyncData(:final value?) ||
|
||||||
AsyncLoading(:final value?) => CustomScrollView(
|
AsyncLoading(:final value?) => CustomScrollView(
|
||||||
|
|
@ -426,7 +426,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
itemBuilder: (_, index) {
|
itemBuilder: (_, index) {
|
||||||
final event = value[index];
|
final event = value[index];
|
||||||
final previousEvent = value.getOrNull(index - 1);
|
final previousEvent = value.getOrNull(index - 1);
|
||||||
return FlashWrapper(
|
return HighlightWrapper(
|
||||||
EventRenderer(
|
EventRenderer(
|
||||||
event,
|
event,
|
||||||
onTapReply: () async {
|
onTapReply: () async {
|
||||||
|
|
@ -440,10 +440,10 @@ class RoomChat extends HookConsumerWidget {
|
||||||
duration: (_) => .new(milliseconds: 700),
|
duration: (_) => .new(milliseconds: 700),
|
||||||
curve: (_) => Curves.easeInOut,
|
curve: (_) => Curves.easeInOut,
|
||||||
);
|
);
|
||||||
flashingEvent.value = replyId;
|
highlightedEvent.value = replyId;
|
||||||
await Future.delayed(.new(seconds: 1), () {
|
await Future.delayed(.new(seconds: 1), () {
|
||||||
if (flashingEvent.value == replyId) {
|
if (highlightedEvent.value == replyId) {
|
||||||
flashingEvent.value = null;
|
highlightedEvent.value = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -457,8 +457,8 @@ class RoomChat extends HookConsumerWidget {
|
||||||
"${event.sender}${event.pmp?.id}" ==
|
"${event.sender}${event.pmp?.id}" ==
|
||||||
"${previousEvent?.sender}${previousEvent?.pmp?.id}",
|
"${previousEvent?.sender}${previousEvent?.pmp?.id}",
|
||||||
),
|
),
|
||||||
isFlashing:
|
isHighlighted:
|
||||||
flashingEvent.value == event.eventId,
|
highlightedEvent.value == event.eventId,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,7 @@ class Sidebar extends HookConsumerWidget {
|
||||||
},
|
},
|
||||||
short: true,
|
short: true,
|
||||||
icon: AvatarOrHash(
|
icon: AvatarOrHash(
|
||||||
|
height: 28,
|
||||||
space.room?.metadata?.avatar,
|
space.room?.metadata?.avatar,
|
||||||
fallback: space.icon == null
|
fallback: space.icon == null
|
||||||
? null
|
? null
|
||||||
|
|
@ -209,7 +210,11 @@ class Sidebar extends HookConsumerWidget {
|
||||||
actions: [
|
actions: [
|
||||||
RoomMenu(
|
RoomMenu(
|
||||||
selectedSpace.room,
|
selectedSpace.room,
|
||||||
children: selectedSpace.children,
|
children: selectedSpace.children.addAll(
|
||||||
|
selectedSpace.subSpaces
|
||||||
|
.map((element) => element.children)
|
||||||
|
.flattened,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
264
lib/widgets/user_bottom_sheet.dart
Normal file
264
lib/widgets/user_bottom_sheet.dart
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
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,
|
||||||
|
textAlign: .center,
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
userId,
|
||||||
|
textAlign: .center,
|
||||||
|
style: textTheme.titleSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
ref
|
||||||
|
.watch(ProfileController.provider(userId))
|
||||||
|
.betterWhen(
|
||||||
|
loading: () => Text(""),
|
||||||
|
data: (profile) => Column(
|
||||||
|
children: [
|
||||||
|
if (profile.timezone == null && profile.pronouns.isEmpty)
|
||||||
|
Text(""),
|
||||||
|
Wrap(
|
||||||
|
crossAxisAlignment: .center,
|
||||||
|
alignment: .center,
|
||||||
|
spacing: 4,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
...profile.pronouns
|
||||||
|
.where(
|
||||||
|
// TODO: Check system language (l10n)
|
||||||
|
(pronoun) => pronoun.language == "en",
|
||||||
|
)
|
||||||
|
.mapIndexed(
|
||||||
|
(index, pronoun) => [
|
||||||
|
if (index != 0)
|
||||||
|
Icon(
|
||||||
|
Icons.circle,
|
||||||
|
size: 4,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
pronoun.summary,
|
||||||
|
textAlign: .center,
|
||||||
|
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!,
|
||||||
|
textAlign: .center,
|
||||||
|
style: textTheme.titleSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 8),
|
||||||
|
if (userId != ref.watch(ClientStateController.provider)?.userId &&
|
||||||
|
roomId != null) ...[
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: M3EButton.icon(
|
||||||
|
onPressed: null,
|
||||||
|
shape: .square,
|
||||||
|
style: .tonal,
|
||||||
|
icon: Icon(Icons.message),
|
||||||
|
label: Text("Message"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
if (ref.watch(
|
||||||
|
PowerLevelController.provider(
|
||||||
|
.membershipAction(
|
||||||
|
action: .kick,
|
||||||
|
roomId: roomId!,
|
||||||
|
targetUser: userId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) &&
|
||||||
|
member.status == .join ||
|
||||||
|
member.status == .invite)
|
||||||
|
Padding(
|
||||||
|
padding: .only(top: 4),
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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