refactor: Replace user bottom sheet with menu and small dialog

Signed-off-by: Krille <c.kussowski@famedly.com>
pull/1752/head
Krille 7 months ago
parent b6b1d6ddb1
commit a12c48fae6
No known key found for this signature in database
GPG Key ID: E067ECD60F1A0652

@ -9,6 +9,10 @@
"@repeatPassword": {},
"notAnImage": "Not an image file.",
"@notAnImage": {},
"setCustomPermissionLevel": "Set custom permission level",
"setPermissionsLevelDescription": "Please choose a predefined role below or enter a custom permission level between 0 and 100.",
"ignoreUser": "Ignore user",
"normalUser": "Normal user",
"remove": "Remove",
"@remove": {
"type": "String",

@ -50,6 +50,7 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">

@ -69,12 +69,15 @@ abstract class FluffyThemes {
selectionColor: colorScheme.onSurface.withAlpha(128),
selectionHandleColor: colorScheme.secondary,
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
inputDecorationTheme: const InputDecorationTheme(
border: UnderlineInputBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(AppConfig.borderRadius),
topLeft: Radius.circular(AppConfig.borderRadius),
),
),
contentPadding: const EdgeInsets.all(12),
filled: false,
//contentPadding: EdgeInsets.all(12),
filled: true,
),
appBarTheme: AppBarTheme(
toolbarHeight: isColumnMode ? 72 : 56,
@ -130,6 +133,7 @@ extension BubbleColorTheme on ThemeData {
Color get bubbleColor => brightness == Brightness.light
? colorScheme.primary
: colorScheme.primaryContainer;
Color get onBubbleColor => brightness == Brightness.light
? colorScheme.onPrimary
: colorScheme.onPrimaryContainer;

@ -1,21 +1,17 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/message.dart';
import 'package:fluffychat/pages/chat/seen_by_row.dart';
import 'package:fluffychat/pages/chat/typing_indicators.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/utils/account_config.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:flutter/material.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
class ChatEventList extends StatelessWidget {
final ChatController controller;
const ChatEventList({
super.key,
required this.controller,
@ -136,15 +132,8 @@ class ChatEventList extends StatelessWidget {
},
onSwipe: () => controller.replyAction(replyTo: event),
onInfoTab: controller.showEventInfo,
onAvatarTab: (Event event) => showAdaptiveBottomSheet(
context: context,
builder: (c) => UserBottomSheet(
user: event.senderFromMemoryOrFallback,
outerContext: context,
onMention: () => controller.sendController.text +=
'${event.senderFromMemoryOrFallback.mention} ',
),
),
onMention: () => controller.sendController.text +=
'${event.senderFromMemoryOrFallback.mention} ',
highlightMarker:
controller.scrollToEventIdMarker == event.eventId,
onSelect: controller.onSelectMessage,

@ -1,12 +1,5 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:swipe_to_action/swipe_to_action.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/events/room_creation_state_event.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
@ -14,6 +7,13 @@ import 'package:fluffychat/utils/file_description.dart';
import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/member_actions_popup_menu_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:swipe_to_action/swipe_to_action.dart';
import '../../../config/app_config.dart';
import 'message_content.dart';
import 'message_reactions.dart';
@ -27,10 +27,10 @@ class Message extends StatelessWidget {
final Event? previousEvent;
final bool displayReadMarker;
final void Function(Event) onSelect;
final void Function(Event) onAvatarTab;
final void Function(Event) onInfoTab;
final void Function(String) scrollToEventId;
final void Function() onSwipe;
final void Function() onMention;
final bool longPressSelect;
final bool selected;
final Timeline timeline;
@ -49,7 +49,6 @@ class Message extends StatelessWidget {
this.longPressSelect = false,
required this.onSelect,
required this.onInfoTab,
required this.onAvatarTab,
required this.scrollToEventId,
required this.onSwipe,
this.selected = false,
@ -58,6 +57,7 @@ class Message extends StatelessWidget {
this.animateIn = false,
this.resetAnimateIn,
this.wallpaperMode = false,
required this.onMention,
required this.scrollController,
required this.colors,
super.key,
@ -236,13 +236,16 @@ class Message extends StatelessWidget {
builder: (context, snapshot) {
final user = snapshot.data ??
event.senderFromMemoryOrFallback;
return Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
presenceUserId: user.stateKey,
presenceBackgroundColor:
wallpaperMode ? Colors.transparent : null,
onTap: () => onAvatarTab(event),
return MemberActionsPopupMenuButton(
onMention: onMention,
user: user,
child: Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
presenceUserId: user.stateKey,
presenceBackgroundColor:
wallpaperMode ? Colors.transparent : null,
),
);
},
),

@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
class RoomCreationStateEvent extends StatelessWidget {
final Event event;

@ -1,10 +1,3 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/pages/chat_details/participant_list_item.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
@ -13,6 +6,12 @@ import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import '../../utils/url_launcher.dart';
import '../../widgets/qr_code_viewer.dart';
@ -37,6 +36,8 @@ class ChatDetailsView extends StatelessWidget {
);
}
final directChatMatrixID = room.directChatMatrixID;
return StreamBuilder(
stream: room.client.onRoomState.stream
.where((update) => update.roomId == room.id),
@ -57,7 +58,7 @@ class ChatDetailsView extends StatelessWidget {
const Center(child: BackButton()),
elevation: theme.appBarTheme.elevation,
actions: <Widget>[
if (room.canonicalAlias.isNotEmpty) ...[
if (room.canonicalAlias.isNotEmpty)
IconButton(
tooltip: L10n.of(context).share,
icon: const Icon(Icons.qr_code_rounded),
@ -65,8 +66,16 @@ class ChatDetailsView extends StatelessWidget {
context,
room.canonicalAlias,
),
)
else if (directChatMatrixID != null)
IconButton(
tooltip: L10n.of(context).share,
icon: const Icon(Icons.qr_code_rounded),
onPressed: () => showQrCodeViewer(
context,
directChatMatrixID,
),
),
],
if (controller.widget.embeddedCloseButton == null)
ChatSettingsPopupMenu(room, false),
],

@ -1,12 +1,10 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/widgets/member_actions_popup_menu_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import '../../widgets/avatar.dart';
import '../user_bottom_sheet/user_bottom_sheet.dart';
class ParticipantListItem extends StatelessWidget {
final User user;
@ -33,72 +31,68 @@ class ParticipantListItem extends StatelessWidget {
return Opacity(
opacity: user.membership == Membership.join ? 1 : 0.5,
child: ListTile(
onTap: () => showAdaptiveBottomSheet(
context: context,
builder: (c) => UserBottomSheet(
user: user,
outerContext: context,
),
),
title: Row(
children: <Widget>[
Expanded(
child: Text(
user.calcDisplayname(),
overflow: TextOverflow.ellipsis,
),
),
if (permissionBatch.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
child: MemberActionsPopupMenuButton(
user: user,
child: ListTile(
title: Row(
children: <Widget>[
Expanded(
child: Text(
user.calcDisplayname(),
overflow: TextOverflow.ellipsis,
),
decoration: BoxDecoration(
color: user.powerLevel >= 100
? theme.colorScheme.tertiary
: theme.colorScheme.tertiaryContainer,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
if (permissionBatch.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
),
child: Text(
permissionBatch,
style: theme.textTheme.labelSmall?.copyWith(
decoration: BoxDecoration(
color: user.powerLevel >= 100
? theme.colorScheme.onTertiary
: theme.colorScheme.onTertiaryContainer,
? theme.colorScheme.tertiary
: theme.colorScheme.tertiaryContainer,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
),
),
membershipBatch == null
? const SizedBox.shrink()
: Container(
padding: const EdgeInsets.all(4),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: theme.secondaryHeaderColor,
borderRadius: BorderRadius.circular(8),
child: Text(
permissionBatch,
style: theme.textTheme.labelSmall?.copyWith(
color: user.powerLevel >= 100
? theme.colorScheme.onTertiary
: theme.colorScheme.onTertiaryContainer,
),
child: Center(
child: Text(
membershipBatch,
style: theme.textTheme.labelSmall,
),
),
membershipBatch == null
? const SizedBox.shrink()
: Container(
padding: const EdgeInsets.all(4),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: theme.secondaryHeaderColor,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
membershipBatch,
style: theme.textTheme.labelSmall,
),
),
),
),
],
),
subtitle: Text(
user.id,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
leading: Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
presenceUserId: user.stateKey,
],
),
subtitle: Text(
user.id,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
leading: Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
presenceUserId: user.stateKey,
),
),
),
);

@ -1,9 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
@ -11,13 +5,18 @@ import 'package:fluffychat/pages/chat_list/dummy_chat_list_item.dart';
import 'package:fluffychat/pages/chat_list/search_title.dart';
import 'package:fluffychat/pages/chat_list/space_view.dart';
import 'package:fluffychat/pages/chat_list/status_msg_list.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import '../../config/themes.dart';
import '../../widgets/adaptive_dialogs/user_dialog.dart';
import '../../widgets/matrix.dart';
import 'chat_list_header.dart';
@ -117,12 +116,9 @@ class ChatListViewBody extends StatelessWidget {
.results[i].userId.localpart ??
L10n.of(context).unknownDevice,
avatar: userSearchResult.results[i].avatarUrl,
onPressed: () => showAdaptiveBottomSheet(
onPressed: () => UserDialog.show(
context: context,
builder: (c) => UserBottomSheet(
profile: userSearchResult.results[i],
outerContext: context,
),
profile: userSearchResult.results[i],
),
),
),

@ -4,15 +4,15 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../widgets/adaptive_dialogs/user_dialog.dart';
class StatusMessageList extends StatelessWidget {
final void Function() onStatusEdit;
const StatusMessageList({
required this.onStatusEdit,
super.key,
@ -24,12 +24,9 @@ class StatusMessageList extends StatelessWidget {
final client = Matrix.of(context).client;
if (profile.userId == client.userID) return onStatusEdit();
showAdaptiveBottomSheet(
UserDialog.show(
context: context,
builder: (c) => UserBottomSheet(
profile: profile,
outerContext: context,
),
profile: profile,
);
return;
}
@ -290,6 +287,7 @@ extension on CachedPresence {
(currentlyActive == true
? DateTime.now()
: DateTime.fromMillisecondsSinceEpoch(0));
LinearGradient get gradient => presence.isOnline == true
? LinearGradient(
colors: [

@ -1,14 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import '../../widgets/adaptive_dialogs/user_dialog.dart';
class InvitationSelectionView extends StatelessWidget {
final InvitationSelectionController controller;
@ -170,13 +168,9 @@ class _InviteContactListTile extends StatelessWidget {
mxContent: profile.avatarUrl,
name: profile.displayName,
presenceUserId: profile.userId,
onTap: () => showAdaptiveBottomSheet(
onTap: () => UserDialog.show(
context: context,
builder: (c) => UserBottomSheet(
user: user,
profile: profile,
outerContext: context,
),
profile: profile,
),
),
title: Text(

@ -1,20 +1,19 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/new_private_chat/new_private_chat_view.dart';
import 'package:fluffychat/pages/new_private_chat/qr_scanner_modal.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import '../../widgets/adaptive_dialogs/user_dialog.dart';
class NewPrivateChat extends StatefulWidget {
const NewPrivateChat({super.key});
@ -98,12 +97,9 @@ class NewPrivateChatController extends State<NewPrivateChat> {
);
}
void openUserModal(Profile profile) => showAdaptiveBottomSheet(
void openUserModal(Profile profile) => UserDialog.show(
context: context,
builder: (c) => UserBottomSheet(
profile: profile,
outerContext: context,
),
profile: profile,
);
@override

@ -1,285 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/permission_slider_dialog.dart';
import '../../widgets/matrix.dart';
import 'user_bottom_sheet_view.dart';
enum UserBottomSheetAction {
report,
mention,
ban,
kick,
unban,
message,
ignore,
}
class LoadProfileBottomSheet extends StatelessWidget {
final String userId;
final BuildContext outerContext;
const LoadProfileBottomSheet({
super.key,
required this.userId,
required this.outerContext,
});
@override
Widget build(BuildContext context) {
return FutureBuilder<ProfileInformation>(
future: Matrix.of(outerContext)
.client
.getUserProfile(userId)
.timeout(const Duration(seconds: 3)),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done &&
snapshot.data != null) {
return Scaffold(
appBar: AppBar(
leading: CloseButton(
onPressed: Navigator.of(context, rootNavigator: false).pop,
),
),
body: const Center(
child: CircularProgressIndicator.adaptive(),
),
);
}
return UserBottomSheet(
outerContext: outerContext,
profile: Profile(
userId: userId,
avatarUrl: snapshot.data?.avatarUrl,
displayName: snapshot.data?.displayname,
),
profileSearchError: snapshot.error,
);
},
);
}
}
class UserBottomSheet extends StatefulWidget {
final User? user;
final Profile? profile;
final Function? onMention;
final BuildContext outerContext;
final Object? profileSearchError;
const UserBottomSheet({
super.key,
this.user,
this.profile,
required this.outerContext,
this.onMention,
this.profileSearchError,
}) : assert(user != null || profile != null);
@override
UserBottomSheetController createState() => UserBottomSheetController();
}
class UserBottomSheetController extends State<UserBottomSheet> {
void participantAction(UserBottomSheetAction action) async {
final user = widget.user;
final userId = user?.id ?? widget.profile?.userId;
if (userId == null) throw ('user or profile must not be null!');
switch (action) {
case UserBottomSheetAction.report:
if (user == null) throw ('User must not be null for this action!');
final score = await showModalActionPopup<int>(
context: context,
title: L10n.of(context).reportUser,
message: L10n.of(context).howOffensiveIsThisContent,
cancelLabel: L10n.of(context).cancel,
actions: [
AdaptiveModalAction(
value: -100,
label: L10n.of(context).extremeOffensive,
),
AdaptiveModalAction(
value: -50,
label: L10n.of(context).offensive,
),
AdaptiveModalAction(
value: 0,
label: L10n.of(context).inoffensive,
),
],
);
if (score == null) return;
final reason = await showTextInputDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).whyDoYouWantToReportThis,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
hintText: L10n.of(context).reason,
);
if (reason == null || reason.isEmpty) return;
final result = await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(widget.outerContext).client.reportEvent(
user.room.id,
user.id,
reason: reason,
score: score,
),
);
if (result.error != null) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).contentHasBeenReported)),
);
break;
case UserBottomSheetAction.mention:
if (user == null) throw ('User must not be null for this action!');
Navigator.of(context).pop();
widget.onMention!();
break;
case UserBottomSheetAction.ban:
if (user == null) throw ('User must not be null for this action!');
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes,
cancelLabel: L10n.of(context).no,
message: L10n.of(context).banUserDescription,
) ==
OkCancelResult.ok) {
await showFutureLoadingDialog(
context: context,
future: () => user.ban(),
);
Navigator.of(context).pop();
}
break;
case UserBottomSheetAction.unban:
if (user == null) throw ('User must not be null for this action!');
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes,
cancelLabel: L10n.of(context).no,
message: L10n.of(context).unbanUserDescription,
) ==
OkCancelResult.ok) {
await showFutureLoadingDialog(
context: context,
future: () => user.unban(),
);
Navigator.of(context).pop();
}
break;
case UserBottomSheetAction.kick:
if (user == null) throw ('User must not be null for this action!');
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes,
cancelLabel: L10n.of(context).no,
message: L10n.of(context).kickUserDescription,
) ==
OkCancelResult.ok) {
await showFutureLoadingDialog(
context: context,
future: () => user.kick(),
);
Navigator.of(context).pop();
}
break;
case UserBottomSheetAction.message:
Navigator.of(context).pop();
// Workaround for https://github.com/flutter/flutter/issues/27495
await Future.delayed(FluffyThemes.animationDuration);
final roomIdResult = await showFutureLoadingDialog(
context: widget.outerContext,
future: () => Matrix.of(widget.outerContext)
.client
.startDirectChat(user?.id ?? widget.profile!.userId),
);
final roomId = roomIdResult.result;
if (roomId == null) return;
widget.outerContext.go('/rooms/$roomId');
break;
case UserBottomSheetAction.ignore:
Navigator.of(context).pop();
// Workaround for https://github.com/flutter/flutter/issues/27495
await Future.delayed(FluffyThemes.animationDuration);
final userId = user?.id ?? widget.profile?.userId;
widget.outerContext
.go('/rooms/settings/security/ignorelist', extra: userId);
}
}
Object? sendError;
final TextEditingController sendController = TextEditingController();
void knockAccept() async {
final user = widget.user!;
final result = await showFutureLoadingDialog(
context: context,
future: () => user.room.invite(user.id),
);
if (result.error != null) return;
Navigator.of(context).pop();
}
void knockDecline() async {
final user = widget.user!;
final result = await showFutureLoadingDialog(
context: context,
future: () => user.room.kick(user.id),
);
if (result.error != null) return;
Navigator.of(context).pop();
}
void setPowerLevel(int? newLevel) async {
final user = widget.user;
if (user == null) throw ('User must not be null for this action!');
final level = newLevel ??
await showPermissionChooser(
context,
currentLevel: user.powerLevel,
);
if (level == null) return;
if (level == 100) {
final consent = await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes,
cancelLabel: L10n.of(context).no,
message: L10n.of(context).makeAdminDescription,
);
if (consent != OkCancelResult.ok) return;
}
await showFutureLoadingDialog(
context: context,
future: () => user.setPower(level),
);
}
@override
Widget build(BuildContext context) => UserBottomSheetView(this);
}

@ -1,374 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/presence_builder.dart';
import 'package:fluffychat/widgets/qr_code_viewer.dart';
import '../../widgets/matrix.dart';
import 'user_bottom_sheet.dart';
class UserBottomSheetView extends StatelessWidget {
final UserBottomSheetController controller;
const UserBottomSheetView(this.controller, {super.key});
@override
Widget build(BuildContext context) {
final user = controller.widget.user;
final userId = (user?.id ?? controller.widget.profile?.userId)!;
final displayname = (user?.calcDisplayname() ??
controller.widget.profile?.displayName ??
controller.widget.profile?.userId.localpart)!;
final avatarUrl = user?.avatarUrl ?? controller.widget.profile?.avatarUrl;
final client = Matrix.of(controller.widget.outerContext).client;
final profileSearchError = controller.widget.profileSearchError;
final dmRoomId = client.getDirectChatFromUserId(userId);
return Scaffold(
appBar: AppBar(
leading: Center(
child: CloseButton(
onPressed: Navigator.of(context, rootNavigator: false).pop,
),
),
centerTitle: false,
title: Text(displayname),
actions: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: IconButton(
onPressed: () => showQrCodeViewer(context, userId),
icon: const Icon(Icons.qr_code_outlined),
),
),
],
),
body: StreamBuilder<Object>(
stream: user?.room.client.onSync.stream.where(
(syncUpdate) =>
syncUpdate.rooms?.join?[user.room.id]?.timeline?.events?.any(
(state) => state.type == EventTypes.RoomPowerLevels,
) ??
false,
),
builder: (context, snapshot) {
final theme = Theme.of(context);
return ListView(
children: [
if (user?.membership == Membership.knock)
Padding(
padding: const EdgeInsets.all(12.0),
child: Material(
color: theme.colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: ListTile(
minVerticalPadding: 16,
title: Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Text(
L10n.of(context)
.userWouldLikeToChangeTheChat(displayname),
),
),
subtitle: Row(
children: [
TextButton.icon(
style: TextButton.styleFrom(
backgroundColor: theme.colorScheme.surface,
foregroundColor: theme.colorScheme.primary,
iconColor: theme.colorScheme.primary,
),
onPressed: controller.knockAccept,
icon: const Icon(Icons.check_outlined),
label: Text(L10n.of(context).accept),
),
const SizedBox(width: 12),
TextButton.icon(
style: TextButton.styleFrom(
backgroundColor: theme.colorScheme.errorContainer,
foregroundColor:
theme.colorScheme.onErrorContainer,
iconColor: theme.colorScheme.onErrorContainer,
),
onPressed: controller.knockDecline,
icon: const Icon(Icons.cancel_outlined),
label: Text(L10n.of(context).decline),
),
],
),
),
),
),
Row(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Avatar(
client: Matrix.of(controller.widget.outerContext).client,
mxContent: avatarUrl,
name: displayname,
size: Avatar.defaultSize * 2.5,
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextButton.icon(
onPressed: () => FluffyShare.share(
userId,
context,
copyOnly: true,
),
icon: const Icon(
Icons.copy_outlined,
size: 14,
),
style: TextButton.styleFrom(
foregroundColor: theme.colorScheme.onSurface,
iconColor: theme.colorScheme.onSurface,
),
label: Text(
userId,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
PresenceBuilder(
userId: userId,
client: client,
builder: (context, presence) {
if (presence == null ||
(presence.presence == PresenceType.offline &&
presence.lastActiveTimestamp == null)) {
return const SizedBox.shrink();
}
final dotColor = presence.presence.isOnline
? Colors.green
: presence.presence.isUnavailable
? Colors.orange
: Colors.grey;
final lastActiveTimestamp =
presence.lastActiveTimestamp;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 16),
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: dotColor,
borderRadius: BorderRadius.circular(16),
),
),
const SizedBox(width: 12),
if (presence.currentlyActive == true)
Text(
L10n.of(context).currentlyActive,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall,
)
else if (lastActiveTimestamp != null)
Text(
L10n.of(context).lastActiveAgo(
lastActiveTimestamp
.localizedTimeShort(context),
),
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall,
),
],
);
},
),
],
),
),
],
),
PresenceBuilder(
userId: userId,
client: client,
builder: (context, presence) {
final status = presence?.statusMsg;
if (status == null || status.isEmpty) {
return const SizedBox.shrink();
}
return ListTile(
title: SelectableLinkify(
text: status,
style: const TextStyle(fontSize: 16),
options: const LinkifyOptions(humanize: false),
linkStyle: const TextStyle(
color: Colors.blueAccent,
decorationColor: Colors.blueAccent,
),
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
),
);
},
),
if (userId != client.userID)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: ElevatedButton.icon(
onPressed: () => controller.participantAction(
UserBottomSheetAction.message,
),
icon: const Icon(Icons.forum_outlined),
label: Text(
dmRoomId == null
? L10n.of(context).startConversation
: L10n.of(context).sendAMessage,
),
),
),
if (controller.widget.onMention != null)
ListTile(
leading: const Icon(Icons.alternate_email_outlined),
title: Text(L10n.of(context).mention),
onTap: () => controller
.participantAction(UserBottomSheetAction.mention),
),
if (user != null) ...[
Divider(color: theme.dividerColor),
ListTile(
title: Text(L10n.of(context).userRole),
leading: const Icon(Icons.admin_panel_settings_outlined),
trailing: Material(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius / 2),
color: theme.colorScheme.onInverseSurface,
child: DropdownButton<int>(
onChanged: user.canChangeUserPowerLevel ||
// Workaround until https://github.com/famedly/matrix-dart-sdk/pull/1765
(user.room.canChangePowerLevel &&
user.id == user.room.client.userID)
? controller.setPowerLevel
: null,
value: {0, 50, 100}.contains(user.powerLevel)
? user.powerLevel
: null,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
borderRadius:
BorderRadius.circular(AppConfig.borderRadius / 2),
underline: const SizedBox.shrink(),
items: [
DropdownMenuItem(
value: 0,
child: Text(
L10n.of(context).userLevel(
user.powerLevel < 50 ? user.powerLevel : 0,
),
),
),
DropdownMenuItem(
value: 50,
child: Text(
L10n.of(context).moderatorLevel(
user.powerLevel >= 50 && user.powerLevel < 100
? user.powerLevel
: 50,
),
),
),
DropdownMenuItem(
value: 100,
child: Text(
L10n.of(context).adminLevel(
user.powerLevel >= 100 ? user.powerLevel : 100,
),
),
),
DropdownMenuItem(
value: null,
child: Text(L10n.of(context).custom),
),
],
),
),
),
],
Divider(color: theme.dividerColor),
if (user != null && user.canKick)
ListTile(
textColor: theme.colorScheme.error,
iconColor: theme.colorScheme.error,
title: Text(L10n.of(context).kickFromChat),
leading: const Icon(Icons.exit_to_app_outlined),
onTap: () =>
controller.participantAction(UserBottomSheetAction.kick),
),
if (user != null &&
user.canBan &&
user.membership != Membership.ban)
ListTile(
textColor: theme.colorScheme.onErrorContainer,
iconColor: theme.colorScheme.onErrorContainer,
title: Text(L10n.of(context).banFromChat),
leading: const Icon(Icons.warning_sharp),
onTap: () =>
controller.participantAction(UserBottomSheetAction.ban),
)
else if (user != null &&
user.canBan &&
user.membership == Membership.ban)
ListTile(
title: Text(L10n.of(context).unbanFromChat),
leading: const Icon(Icons.warning_outlined),
onTap: () =>
controller.participantAction(UserBottomSheetAction.unban),
),
if (user != null && user.id != client.userID)
ListTile(
textColor: theme.colorScheme.onErrorContainer,
iconColor: theme.colorScheme.onErrorContainer,
title: Text(L10n.of(context).reportUser),
leading: const Icon(Icons.gavel_outlined),
onTap: () => controller
.participantAction(UserBottomSheetAction.report),
),
if (profileSearchError != null)
ListTile(
leading: const Icon(
Icons.warning_outlined,
color: Colors.orange,
),
subtitle: Text(
L10n.of(context).profileNotFound,
style: const TextStyle(color: Colors.orange),
),
),
if (userId != client.userID &&
!client.ignoredUsers.contains(userId))
ListTile(
textColor: theme.colorScheme.onErrorContainer,
iconColor: theme.colorScheme.onErrorContainer,
leading: const Icon(Icons.block_outlined),
title: Text(L10n.of(context).block),
onTap: () => controller
.participantAction(UserBottomSheetAction.ignore),
),
],
);
},
),
);
}
}

@ -1,19 +1,18 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:punycode/punycode.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/user_dialog.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:punycode/punycode.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'platform_infos.dart';
class UrlLauncher {
@ -221,13 +220,22 @@ class UrlLauncher {
}
}
} else if (identityParts.primaryIdentifier.sigil == '@') {
await showAdaptiveBottomSheet(
final userId = identityParts.primaryIdentifier;
var noProfileWarning = false;
final profileResult = await showFutureLoadingDialog(
context: context,
builder: (c) => LoadProfileBottomSheet(
userId: identityParts.primaryIdentifier,
outerContext: context,
future: () => matrix.client.getProfileFromUserId(userId).catchError(
(_) {
noProfileWarning = true;
return Profile(userId: userId);
},
),
);
await UserDialog.show(
context: context,
profile: profileResult.result!,
noProfileWarning: noProfileWarning,
);
}
}
}

@ -5,12 +5,14 @@ class AdaptiveDialogAction extends StatelessWidget {
final VoidCallback? onPressed;
final bool autofocus;
final Widget child;
final bool bigButtons;
const AdaptiveDialogAction({
super.key,
required this.onPressed,
required this.child,
this.autofocus = false,
this.bigButtons = false,
});
@override
@ -21,6 +23,27 @@ class AdaptiveDialogAction extends StatelessWidget {
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
if (bigButtons) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: autofocus
? theme.colorScheme.primary
: theme.colorScheme.primaryContainer,
foregroundColor: autofocus
? theme.colorScheme.onPrimary
: theme.colorScheme.onPrimaryContainer,
),
onPressed: onPressed,
autofocus: autofocus,
child: child,
),
),
);
}
return TextButton(
onPressed: onPressed,
autofocus: autofocus,

@ -0,0 +1,180 @@
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/presence_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import '../../utils/url_launcher.dart';
import '../future_loading_dialog.dart';
import '../hover_builder.dart';
import '../matrix.dart';
class UserDialog extends StatelessWidget {
static Future<void> show({
required BuildContext context,
required Profile profile,
bool noProfileWarning = false,
}) =>
showAdaptiveDialog(
context: context,
builder: (context) => UserDialog(
profile,
noProfileWarning: noProfileWarning,
),
);
final Profile profile;
final bool noProfileWarning;
const UserDialog(this.profile, {this.noProfileWarning = false, super.key});
@override
Widget build(BuildContext context) {
final client = Matrix.of(context).client;
final dmRoomId = client.getDirectChatFromUserId(profile.userId);
final displayname = profile.displayName ??
profile.userId.localpart ??
L10n.of(context).user;
var copied = false;
final theme = Theme.of(context);
return AlertDialog.adaptive(
title: Center(child: Text(displayname, textAlign: TextAlign.center)),
content: SelectionArea(
child: PresenceBuilder(
userId: profile.userId,
client: Matrix.of(context).client,
builder: (context, presence) {
if (presence == null) return const SizedBox.shrink();
final statusMsg = presence.statusMsg;
final lastActiveTimestamp = presence.lastActiveTimestamp;
final presenceText = presence.currentlyActive == true
? L10n.of(context).currentlyActive
: lastActiveTimestamp != null
? L10n.of(context).lastActiveAgo(
lastActiveTimestamp.localizedTimeShort(context),
)
: null;
return Column(
spacing: 8,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
HoverBuilder(
builder: (context, hovered) => StatefulBuilder(
builder: (context, setState) => GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: profile.userId));
setState(() {
copied = true;
});
},
child: RichText(
text: TextSpan(
children: [
WidgetSpan(
child: Padding(
padding: const EdgeInsets.only(right: 4.0),
child: AnimatedScale(
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),
),
textAlign: TextAlign.center,
),
),
),
),
Avatar(
mxContent: profile.avatarUrl,
name: displayname,
size: Avatar.defaultSize * 2,
),
if (presenceText != null)
Text(
presenceText,
style: const TextStyle(fontSize: 10),
),
if (statusMsg != null)
Linkify(
text: statusMsg,
textAlign: TextAlign.center,
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(),
),
],
);
},
),
),
actions: [
if (client.userID != profile.userId) ...[],
AdaptiveDialogAction(
bigButtons: true,
onPressed: () async {
final router = GoRouter.of(context);
Navigator.of(context).pop();
final roomIdResult = await showFutureLoadingDialog(
context: context,
future: () => client.startDirectChat(profile.userId),
);
final roomId = roomIdResult.result;
if (roomId == null) return;
router.go('/rooms/$roomId');
},
child: Text(
dmRoomId == null
? L10n.of(context).startConversation
: L10n.of(context).sendAMessage,
),
),
AdaptiveDialogAction(
bigButtons: true,
onPressed: () {
final router = GoRouter.of(context);
Navigator.of(context).pop();
router.go(
'/rooms/settings/security/ignorelist',
extra: profile.userId,
);
},
child: Text(
L10n.of(context).ignoreUser,
style: TextStyle(color: theme.colorScheme.error),
),
),
AdaptiveDialogAction(
bigButtons: true,
onPressed: Navigator.of(context).pop,
child: Text(L10n.of(context).close),
),
],
);
}
}

@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
import 'package:fluffychat/widgets/presence_builder.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
class Avatar extends StatelessWidget {
final Uri? mxContent;

@ -20,6 +20,7 @@ Future<Result<T>> showFutureLoadingDialog<T>({
bool barrierDismissible = false,
bool delay = true,
ExceptionContext? exceptionContext,
bool ignoreError = false,
}) async {
final futureExec = future();
final resultFuture = ResultFuture(futureExec);
@ -67,6 +68,7 @@ class LoadingDialog<T> extends StatefulWidget {
this.backLabel,
this.exceptionContext,
});
@override
LoadingDialogState<T> createState() => LoadingDialogState<T>();
}

@ -0,0 +1,296 @@
import 'package:fluffychat/widgets/permission_slider_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'adaptive_dialogs/show_modal_action_popup.dart';
import 'adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'adaptive_dialogs/show_text_input_dialog.dart';
import 'adaptive_dialogs/user_dialog.dart';
import 'avatar.dart';
import 'future_loading_dialog.dart';
class MemberActionsPopupMenuButton extends StatelessWidget {
final Widget child;
final User user;
final void Function()? onMention;
const MemberActionsPopupMenuButton({
required this.child,
required this.user,
this.onMention,
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final displayname = user.calcDisplayname();
final isMe = user.room.client.userID == user.id;
return PopupMenuButton(
onSelected: (action) async {
switch (action) {
case _MemberActions.mention:
onMention?.call();
return;
case _MemberActions.setRole:
final power = await showPermissionChooser(
context,
currentLevel: user.powerLevel,
maxLevel: user.room.ownPowerLevel,
);
if (power == null) return;
if (!context.mounted) return;
if (power >= 100) {
final consent = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).areYouSure,
message: L10n.of(context).makeAdminDescription,
);
if (consent != OkCancelResult.ok) return;
if (!context.mounted) return;
}
await showFutureLoadingDialog(
context: context,
future: () => user.setPower(power),
);
return;
case _MemberActions.kick:
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes,
cancelLabel: L10n.of(context).no,
message: L10n.of(context).kickUserDescription,
) ==
OkCancelResult.ok) {
await showFutureLoadingDialog(
context: context,
future: () => user.kick(),
);
}
return;
case _MemberActions.ban:
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes,
cancelLabel: L10n.of(context).no,
message: L10n.of(context).banUserDescription,
) ==
OkCancelResult.ok) {
await showFutureLoadingDialog(
context: context,
future: () => user.ban(),
);
}
return;
case _MemberActions.report:
final score = await showModalActionPopup<int>(
context: context,
title: L10n.of(context).reportUser,
message: L10n.of(context).howOffensiveIsThisContent,
cancelLabel: L10n.of(context).cancel,
actions: [
AdaptiveModalAction(
value: -100,
label: L10n.of(context).extremeOffensive,
),
AdaptiveModalAction(
value: -50,
label: L10n.of(context).offensive,
),
AdaptiveModalAction(
value: 0,
label: L10n.of(context).inoffensive,
),
],
);
if (score == null) return;
final reason = await showTextInputDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).whyDoYouWantToReportThis,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
hintText: L10n.of(context).reason,
);
if (reason == null || reason.isEmpty) return;
final result = await showFutureLoadingDialog(
context: context,
future: () => user.room.client.reportEvent(
user.room.id,
user.id,
reason: reason,
score: score,
),
);
if (result.error != null) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).contentHasBeenReported)),
);
return;
case _MemberActions.info:
await UserDialog.show(
context: context,
profile: Profile(
userId: user.id,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
),
);
return;
case _MemberActions.unban:
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes,
cancelLabel: L10n.of(context).no,
message: L10n.of(context).unbanUserDescription,
) ==
OkCancelResult.ok) {
await showFutureLoadingDialog(
context: context,
future: () => user.unban(),
);
}
return;
}
},
itemBuilder: (context) => <PopupMenuEntry<_MemberActions>>[
PopupMenuItem(
value: _MemberActions.info,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Avatar(
name: displayname,
mxContent: user.avatarUrl,
presenceUserId: user.id,
presenceBackgroundColor: theme.colorScheme.surfaceContainer,
),
const SizedBox(height: 8),
Text(
displayname,
textAlign: TextAlign.center,
style: theme.textTheme.labelLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
user.id,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 10),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
const PopupMenuDivider(),
if (onMention != null)
PopupMenuItem(
value: _MemberActions.mention,
child: ListTile(
leading: const Icon(Icons.alternate_email_outlined),
title: Text(L10n.of(context).mention),
),
),
PopupMenuItem(
enabled:
user.room.canChangePowerLevel && user.canChangeUserPowerLevel,
value: _MemberActions.setRole,
child: ListTile(
leading: const Icon(Icons.admin_panel_settings_outlined),
title: Text(L10n.of(context).chatPermissions),
subtitle: Text(
user.powerLevel < 50
? L10n.of(context).userLevel(user.powerLevel)
: user.powerLevel < 100
? L10n.of(context).moderatorLevel(user.powerLevel)
: L10n.of(context).adminLevel(user.powerLevel),
style: const TextStyle(fontSize: 10),
),
),
),
if (user.canKick)
PopupMenuItem(
value: _MemberActions.kick,
child: ListTile(
leading: Icon(
Icons.person_remove_outlined,
color: theme.colorScheme.onErrorContainer,
),
title: Text(
L10n.of(context).kickFromChat,
style: TextStyle(color: theme.colorScheme.onErrorContainer),
),
),
),
if (user.canBan)
PopupMenuItem(
value: _MemberActions.ban,
child: ListTile(
leading: Icon(
Icons.block_outlined,
color: theme.colorScheme.onErrorContainer,
),
title: Text(
L10n.of(context).banFromChat,
style: TextStyle(color: theme.colorScheme.onErrorContainer),
),
),
),
if (user.canBan && user.membership == Membership.ban)
PopupMenuItem(
value: _MemberActions.ban,
child: ListTile(
leading: const Icon(Icons.warning),
title: Text(
L10n.of(context).unbanFromChat,
),
),
),
if (user.canBan && user.membership == Membership.ban)
PopupMenuItem(
value: _MemberActions.unban,
child: ListTile(
leading: const Icon(Icons.warning_outlined),
title: Text(L10n.of(context).unbanFromChat),
),
),
if (!isMe)
PopupMenuItem(
value: _MemberActions.report,
child: ListTile(
leading: Icon(
Icons.gavel_outlined,
color: theme.colorScheme.onErrorContainer,
),
title: Text(
L10n.of(context).reportUser,
style: TextStyle(color: theme.colorScheme.onErrorContainer),
),
),
),
],
child: child,
);
}
}
enum _MemberActions {
info,
mention,
setRole,
kick,
ban,
unban,
report,
}

@ -1,31 +1,72 @@
import 'package:flutter/cupertino.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/dialog_text_field.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart';
Future<int?> showPermissionChooser(
BuildContext context, {
int currentLevel = 0,
int maxLevel = 100,
}) async {
final customLevel = await showTextInputDialog(
final controller = TextEditingController();
final error = ValueNotifier<String?>(null);
return await showAdaptiveDialog<int>(
context: context,
title: L10n.of(context).setPermissionsLevel,
initialText: currentLevel.toString(),
keyboardType: TextInputType.number,
autocorrect: false,
validator: (text) {
if (text.isEmpty) {
return L10n.of(context).pleaseEnterANumber;
}
final level = int.tryParse(text);
if (level == null) {
return L10n.of(context).pleaseEnterANumber;
}
return null;
},
builder: (context) => AlertDialog.adaptive(
title: Text(L10n.of(context).chatPermissions),
content: Column(
mainAxisSize: MainAxisSize.min,
spacing: 12.0,
children: [
Text(L10n.of(context).setPermissionsLevelDescription),
ValueListenableBuilder(
valueListenable: error,
builder: (context, errorText, _) => DialogTextField(
controller: controller,
hintText: currentLevel.toString(),
keyboardType: TextInputType.number,
labelText: L10n.of(context).custom,
errorText: errorText,
),
),
],
),
actions: [
AdaptiveDialogAction(
bigButtons: true,
onPressed: () {
final level = int.tryParse(controller.text.trim());
if (level == null) {
error.value = L10n.of(context).pleaseEnterANumber;
return;
}
if (level > maxLevel) {
error.value = L10n.of(context).noPermission;
return;
}
Navigator.of(context).pop<int>(level);
},
child: Text(L10n.of(context).setCustomPermissionLevel),
),
if (maxLevel >= 100 && currentLevel != 100)
AdaptiveDialogAction(
bigButtons: true,
onPressed: () => Navigator.of(context).pop<int>(100),
child: Text(L10n.of(context).admin),
),
if (maxLevel >= 50 && currentLevel != 50)
AdaptiveDialogAction(
bigButtons: true,
onPressed: () => Navigator.of(context).pop<int>(50),
child: Text(L10n.of(context).moderator),
),
if (currentLevel != 0)
AdaptiveDialogAction(
bigButtons: true,
onPressed: () => Navigator.of(context).pop<int>(0),
child: Text(L10n.of(context).normalUser),
),
],
),
);
if (customLevel == null) return null;
return int.tryParse(customLevel);
}

Loading…
Cancel
Save