TF-1074 Handle go to path /mailbox_creator
This commit is contained in:
@@ -4,6 +4,10 @@ import 'package:model/mailbox/presentation_mailbox.dart';
|
|||||||
import 'package:tmail_ui_user/features/destination_picker/presentation/destination_picker_bindings.dart';
|
import 'package:tmail_ui_user/features/destination_picker/presentation/destination_picker_bindings.dart';
|
||||||
import 'package:tmail_ui_user/features/destination_picker/presentation/destination_picker_view.dart';
|
import 'package:tmail_ui_user/features/destination_picker/presentation/destination_picker_view.dart';
|
||||||
import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart';
|
import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart';
|
||||||
|
import 'package:tmail_ui_user/features/mailbox_creator/presentation/mailbox_creator_bindings.dart';
|
||||||
|
import 'package:tmail_ui_user/features/mailbox_creator/presentation/mailbox_creator_view.dart';
|
||||||
|
import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/mailbox_creator_arguments.dart';
|
||||||
|
import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/new_mailbox_arguments.dart';
|
||||||
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
||||||
|
|
||||||
mixin ViewAsDialogActionMixin {
|
mixin ViewAsDialogActionMixin {
|
||||||
@@ -19,7 +23,7 @@ mixin ViewAsDialogActionMixin {
|
|||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: true,
|
barrierDismissible: true,
|
||||||
barrierLabel: '',
|
barrierLabel: '',
|
||||||
barrierColor: Colors.black54,
|
barrierColor: Colors.black.withAlpha(24),
|
||||||
pageBuilder: (context, animation, secondaryAnimation) {
|
pageBuilder: (context, animation, secondaryAnimation) {
|
||||||
return DestinationPickerView.fromArguments(
|
return DestinationPickerView.fromArguments(
|
||||||
arguments,
|
arguments,
|
||||||
@@ -37,4 +41,34 @@ mixin ViewAsDialogActionMixin {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showDialogMailboxCreator({
|
||||||
|
required BuildContext context,
|
||||||
|
required MailboxCreatorArguments arguments,
|
||||||
|
required Function(NewMailboxArguments) onCreatedMailbox
|
||||||
|
}) {
|
||||||
|
MailboxCreatorBindings().dependencies();
|
||||||
|
|
||||||
|
showGeneralDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: true,
|
||||||
|
barrierLabel: '',
|
||||||
|
barrierColor: Colors.black.withAlpha(24),
|
||||||
|
pageBuilder: (context, animation, secondaryAnimation) {
|
||||||
|
return MailboxCreatorView.fromArguments(
|
||||||
|
arguments,
|
||||||
|
onDismissCallback: () {
|
||||||
|
MailboxCreatorBindings().dispose();
|
||||||
|
popBack();
|
||||||
|
},
|
||||||
|
onCreatedMailboxCallback: (arguments) {
|
||||||
|
MailboxCreatorBindings().dispose();
|
||||||
|
popBack();
|
||||||
|
|
||||||
|
if (arguments is NewMailboxArguments) {
|
||||||
|
onCreatedMailbox.call(arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -380,20 +380,37 @@ class MailboxController extends BaseMailboxController {
|
|||||||
_openMailboxEventController.add(OpenMailboxViewEvent(context, presentationMailboxSelected));
|
_openMailboxEventController.add(OpenMailboxViewEvent(context, presentationMailboxSelected));
|
||||||
}
|
}
|
||||||
|
|
||||||
void goToCreateNewMailboxView() async {
|
void goToCreateNewMailboxView(BuildContext context) async {
|
||||||
final accountId = mailboxDashBoardController.accountId.value;
|
final accountId = mailboxDashBoardController.accountId.value;
|
||||||
if (accountId != null) {
|
if (accountId != null) {
|
||||||
final newMailboxArguments = await push(
|
final arguments = MailboxCreatorArguments(
|
||||||
AppRoutes.mailboxCreator,
|
accountId,
|
||||||
arguments: MailboxCreatorArguments(accountId, defaultMailboxTree.value, folderMailboxTree.value)
|
defaultMailboxTree.value,
|
||||||
);
|
folderMailboxTree.value);
|
||||||
|
|
||||||
if (newMailboxArguments != null && newMailboxArguments is NewMailboxArguments) {
|
if (BuildUtils.isWeb) {
|
||||||
final generateCreateId = Id(_uuid.v1());
|
showDialogMailboxCreator(
|
||||||
_createNewMailboxAction(accountId, CreateNewMailboxRequest(
|
context: context,
|
||||||
generateCreateId,
|
arguments: arguments,
|
||||||
newMailboxArguments.newName,
|
onCreatedMailbox: (newMailboxArguments) {
|
||||||
parentId: newMailboxArguments.mailboxLocation?.id));
|
final generateCreateId = Id(_uuid.v1());
|
||||||
|
_createNewMailboxAction(accountId, CreateNewMailboxRequest(
|
||||||
|
generateCreateId,
|
||||||
|
newMailboxArguments.newName,
|
||||||
|
parentId: newMailboxArguments.mailboxLocation?.id));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
final newMailboxArguments = await push(
|
||||||
|
AppRoutes.mailboxCreator,
|
||||||
|
arguments: arguments);
|
||||||
|
|
||||||
|
if (newMailboxArguments != null && newMailboxArguments is NewMailboxArguments) {
|
||||||
|
final generateCreateId = Id(_uuid.v1());
|
||||||
|
_createNewMailboxAction(accountId, CreateNewMailboxRequest(
|
||||||
|
generateCreateId,
|
||||||
|
newMailboxArguments.newName,
|
||||||
|
parentId: newMailboxArguments.mailboxLocation?.id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ class MailboxView extends GetWidget<MailboxController> {
|
|||||||
return buildIconWeb(
|
return buildIconWeb(
|
||||||
icon: SvgPicture.asset(_imagePaths.icAddNewFolder, width: 28, height: 24, color: AppColor.colorTextButton, fit: BoxFit.fill),
|
icon: SvgPicture.asset(_imagePaths.icAddNewFolder, width: 28, height: 24, color: AppColor.colorTextButton, fit: BoxFit.fill),
|
||||||
tooltip: AppLocalizations.of(context).new_mailbox,
|
tooltip: AppLocalizations.of(context).new_mailbox,
|
||||||
onTap: () => controller.goToCreateNewMailboxView());
|
onTap: () => controller.goToCreateNewMailboxView(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEditMailboxButton(BuildContext context, bool isSelectionEnabled) {
|
Widget _buildEditMailboxButton(BuildContext context, bool isSelectionEnabled) {
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ class MailboxView extends GetWidget<MailboxController> with AppLoaderMixin, Popu
|
|||||||
splashRadius: 15,
|
splashRadius: 15,
|
||||||
icon: SvgPicture.asset(_imagePaths.icAddNewFolder, color: AppColor.colorTextButton, fit: BoxFit.fill),
|
icon: SvgPicture.asset(_imagePaths.icAddNewFolder, color: AppColor.colorTextButton, fit: BoxFit.fill),
|
||||||
tooltip: AppLocalizations.of(context).new_mailbox,
|
tooltip: AppLocalizations.of(context).new_mailbox,
|
||||||
onTap: () => controller.goToCreateNewMailboxView())),
|
onTap: () => controller.goToCreateNewMailboxView(context))),
|
||||||
]),
|
]),
|
||||||
Obx(() => controller.folderMailboxHasChild
|
Obx(() => controller.folderMailboxHasChild
|
||||||
? _buildMailboxCategory(context, MailboxCategories.folders, controller.folderRootNode)
|
? _buildMailboxCategory(context, MailboxCategories.folders, controller.folderRootNode)
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ class MailboxCreatorBindings extends BaseBindings {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void bindingsController() {
|
void bindingsController() {
|
||||||
Get.lazyPut(() => MailboxCreatorController(
|
Get.lazyPut(() => MailboxCreatorController(Get.find<VerifyNameInteractor>()));
|
||||||
Get.find<VerifyNameInteractor>(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -28,4 +26,8 @@ class MailboxCreatorBindings extends BaseBindings {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void bindingsRepositoryImpl() {}
|
void bindingsRepositoryImpl() {}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
Get.delete<MailboxCreatorController>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,8 @@ import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/new_ma
|
|||||||
import 'package:tmail_ui_user/main/routes/app_routes.dart';
|
import 'package:tmail_ui_user/main/routes/app_routes.dart';
|
||||||
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
||||||
|
|
||||||
|
typedef OnCreatedMailboxCallback = Function(NewMailboxArguments? arguments);
|
||||||
|
|
||||||
class MailboxCreatorController extends BaseController {
|
class MailboxCreatorController extends BaseController {
|
||||||
|
|
||||||
final VerifyNameInteractor _verifyNameInteractor;
|
final VerifyNameInteractor _verifyNameInteractor;
|
||||||
@@ -27,27 +29,34 @@ class MailboxCreatorController extends BaseController {
|
|||||||
final selectedMailbox = Rxn<PresentationMailbox>();
|
final selectedMailbox = Rxn<PresentationMailbox>();
|
||||||
final newNameMailbox = Rxn<String>();
|
final newNameMailbox = Rxn<String>();
|
||||||
|
|
||||||
final nameInputFocusNode = FocusNode();
|
FocusNode? nameInputFocusNode;
|
||||||
|
|
||||||
|
MailboxCreatorArguments? arguments;
|
||||||
AccountId? accountId;
|
AccountId? accountId;
|
||||||
MailboxTree? folderMailboxTree;
|
MailboxTree? folderMailboxTree;
|
||||||
MailboxTree? defaultMailboxTree;
|
MailboxTree? defaultMailboxTree;
|
||||||
|
OnCreatedMailboxCallback? onCreatedMailboxCallback;
|
||||||
|
VoidCallback? onDismissMailboxCreator;
|
||||||
|
|
||||||
List<String> listMailboxNameAsStringExist = <String>[];
|
List<String> listMailboxNameAsStringExist = <String>[];
|
||||||
|
|
||||||
MailboxCreatorController(
|
MailboxCreatorController(this._verifyNameInteractor);
|
||||||
this._verifyNameInteractor,
|
|
||||||
);
|
|
||||||
|
|
||||||
void setNewNameMailbox(String newName) => newNameMailbox.value = newName;
|
void setNewNameMailbox(String newName) => newNameMailbox.value = newName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
nameInputFocusNode = FocusNode();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onReady() {
|
void onReady() {
|
||||||
super.onReady();
|
super.onReady();
|
||||||
final arguments = Get.arguments;
|
if (arguments != null) {
|
||||||
if (arguments is MailboxCreatorArguments) {
|
folderMailboxTree = arguments!.folderMailboxTree;
|
||||||
folderMailboxTree = arguments.folderMailboxTree;
|
defaultMailboxTree = arguments!.defaultMailboxTree;
|
||||||
defaultMailboxTree = arguments.defaultMailboxTree;
|
accountId = arguments!.accountId;
|
||||||
accountId = arguments.accountId;
|
|
||||||
_createListMailboxNameAsStringInMailboxLocation();
|
_createListMailboxNameAsStringInMailboxLocation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,14 +70,14 @@ class MailboxCreatorController extends BaseController {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
nameInputFocusNode.dispose();
|
nameInputFocusNode?.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isCreateMailboxValidated(BuildContext context) {
|
bool isCreateMailboxValidated(BuildContext context) {
|
||||||
final nameValidated = getErrorInputNameString(context);
|
final nameValidated = getErrorInputNameString(context);
|
||||||
|
|
||||||
if (!nameInputFocusNode.hasFocus && newNameMailbox.value == null) {
|
if (nameInputFocusNode?.hasFocus == false && newNameMailbox.value == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,14 +118,12 @@ class MailboxCreatorController extends BaseController {
|
|||||||
listMailboxNameAsStringExist = [];
|
listMailboxNameAsStringExist = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log('MailboxCreatorController::_createListMailboxNameAsStringInMailboxLocation(): ${listMailboxNameAsStringExist.toString()}');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String? getErrorInputNameString(BuildContext context) {
|
String? getErrorInputNameString(BuildContext context) {
|
||||||
final nameMailbox = newNameMailbox.value;
|
final nameMailbox = newNameMailbox.value;
|
||||||
|
|
||||||
if (!nameInputFocusNode.hasFocus && nameMailbox == null) {
|
if (nameInputFocusNode?.hasFocus == false && nameMailbox == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,17 +185,34 @@ class MailboxCreatorController extends BaseController {
|
|||||||
|
|
||||||
void createNewMailbox(BuildContext context) {
|
void createNewMailbox(BuildContext context) {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
|
|
||||||
final nameMailbox = newNameMailbox.value;
|
final nameMailbox = newNameMailbox.value;
|
||||||
if (nameMailbox != null && nameMailbox.isNotEmpty) {
|
if (nameMailbox != null && nameMailbox.isNotEmpty) {
|
||||||
final newMailboxArguments = NewMailboxArguments(
|
final newMailboxArguments = NewMailboxArguments(
|
||||||
MailboxName(nameMailbox),
|
MailboxName(nameMailbox),
|
||||||
mailboxLocation: selectedMailbox.value);
|
mailboxLocation: selectedMailbox.value);
|
||||||
popBack(result: newMailboxArguments);
|
|
||||||
|
if (BuildUtils.isWeb) {
|
||||||
|
nameInputFocusNode?.dispose();
|
||||||
|
nameInputFocusNode = null;
|
||||||
|
|
||||||
|
onCreatedMailboxCallback?.call(newMailboxArguments);
|
||||||
|
} else {
|
||||||
|
popBack(result: newMailboxArguments);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void closeMailboxCreator(BuildContext context) {
|
void closeMailboxCreator(BuildContext context) {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
popBack();
|
|
||||||
|
if (BuildUtils.isWeb) {
|
||||||
|
nameInputFocusNode?.dispose();
|
||||||
|
nameInputFocusNode = null;
|
||||||
|
|
||||||
|
onDismissMailboxCreator?.call();
|
||||||
|
} else {
|
||||||
|
popBack();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
import 'package:core/core.dart';
|
import 'package:core/presentation/extensions/color_extension.dart';
|
||||||
|
import 'package:core/presentation/resources/image_paths.dart';
|
||||||
|
import 'package:core/presentation/utils/responsive_utils.dart';
|
||||||
|
import 'package:core/presentation/utils/style_utils.dart';
|
||||||
|
import 'package:core/presentation/views/text/text_field_builder.dart';
|
||||||
|
import 'package:core/utils/build_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox/domain/extensions/presentation_mailbox_extension.dart';
|
import 'package:tmail_ui_user/features/mailbox/domain/extensions/presentation_mailbox_extension.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox_creator/presentation/mailbox_creator_controller.dart';
|
import 'package:tmail_ui_user/features/mailbox_creator/presentation/mailbox_creator_controller.dart';
|
||||||
|
import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/mailbox_creator_arguments.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox_creator/presentation/widgets/app_bar_mailbox_creator_builder.dart';
|
import 'package:tmail_ui_user/features/mailbox_creator/presentation/widgets/app_bar_mailbox_creator_builder.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox_creator/presentation/widgets/create_mailbox_name_input_decoration_builder.dart';
|
import 'package:tmail_ui_user/features/mailbox_creator/presentation/widgets/create_mailbox_name_input_decoration_builder.dart';
|
||||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||||
@@ -15,7 +21,24 @@ class MailboxCreatorView extends GetWidget<MailboxCreatorController> {
|
|||||||
final _imagePaths = Get.find<ImagePaths>();
|
final _imagePaths = Get.find<ImagePaths>();
|
||||||
final _responsiveUtils = Get.find<ResponsiveUtils>();
|
final _responsiveUtils = Get.find<ResponsiveUtils>();
|
||||||
|
|
||||||
MailboxCreatorView({Key? key}) : super(key: key);
|
@override
|
||||||
|
final controller = Get.find<MailboxCreatorController>();
|
||||||
|
|
||||||
|
MailboxCreatorView({Key? key}) : super(key: key) {
|
||||||
|
controller.arguments = Get.arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
MailboxCreatorView.fromArguments(
|
||||||
|
MailboxCreatorArguments arguments, {
|
||||||
|
Key? key,
|
||||||
|
OnCreatedMailboxCallback? onCreatedMailboxCallback,
|
||||||
|
VoidCallback? onDismissCallback
|
||||||
|
}) : super(key: key) {
|
||||||
|
controller.arguments = arguments;
|
||||||
|
controller.onCreatedMailboxCallback = onCreatedMailboxCallback;
|
||||||
|
controller.onDismissMailboxCreator = onDismissCallback;
|
||||||
|
controller.onInit();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -95,7 +118,7 @@ class MailboxCreatorView extends GetWidget<MailboxCreatorController> {
|
|||||||
..keyboardType(TextInputType.visiblePassword)
|
..keyboardType(TextInputType.visiblePassword)
|
||||||
..cursorColor(AppColor.colorTextButton)
|
..cursorColor(AppColor.colorTextButton)
|
||||||
..maxLines(1)
|
..maxLines(1)
|
||||||
..textStyle(TextStyle(
|
..textStyle(const TextStyle(
|
||||||
color: AppColor.colorNameEmail,
|
color: AppColor.colorNameEmail,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
overflow: CommonTextStyle.defaultTextOverFlow))
|
overflow: CommonTextStyle.defaultTextOverFlow))
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ class AppPages {
|
|||||||
opaque: false,
|
opaque: false,
|
||||||
page: () => DeferredWidget(destination_picker.loadLibrary, () => destination_picker.DestinationPickerView()),
|
page: () => DeferredWidget(destination_picker.loadLibrary, () => destination_picker.DestinationPickerView()),
|
||||||
binding: DestinationPickerBindings()),
|
binding: DestinationPickerBindings()),
|
||||||
|
GetPage(
|
||||||
|
name: AppRoutes.mailboxCreator,
|
||||||
|
opaque: false,
|
||||||
|
page: () => DeferredWidget(mailbox_creator.loadLibrary, () => mailbox_creator.MailboxCreatorView()),
|
||||||
|
binding: MailboxCreatorBindings()),
|
||||||
];
|
];
|
||||||
|
|
||||||
static final pages = [
|
static final pages = [
|
||||||
@@ -61,11 +66,6 @@ class AppPages {
|
|||||||
name: AppRoutes.dashboard,
|
name: AppRoutes.dashboard,
|
||||||
page: () => DeferredWidget(mailbox_dashboard.loadLibrary, () => mailbox_dashboard.MailboxDashBoardView()),
|
page: () => DeferredWidget(mailbox_dashboard.loadLibrary, () => mailbox_dashboard.MailboxDashBoardView()),
|
||||||
binding: MailboxDashBoardBindings()),
|
binding: MailboxDashBoardBindings()),
|
||||||
GetPage(
|
|
||||||
name: AppRoutes.mailboxCreator,
|
|
||||||
opaque: false,
|
|
||||||
page: () => DeferredWidget(mailbox_creator.loadLibrary, () => mailbox_creator.MailboxCreatorView()),
|
|
||||||
binding: MailboxCreatorBindings()),
|
|
||||||
GetPage(
|
GetPage(
|
||||||
name: AppRoutes.settings,
|
name: AppRoutes.settings,
|
||||||
page: () => DeferredWidget(manage_account_dashboard.loadLibrary,
|
page: () => DeferredWidget(manage_account_dashboard.loadLibrary,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ abstract class AppRoutes {
|
|||||||
|
|
||||||
static const composer = '/composer';
|
static const composer = '/composer';
|
||||||
static const destinationPicker = '/destination_picker';
|
static const destinationPicker = '/destination_picker';
|
||||||
static const mailboxCreator = '$dashboard/mailbox_creator';
|
static const mailboxCreator = '/mailbox_creator';
|
||||||
static const contact = '$dashboard/contact';
|
static const contact = '$dashboard/contact';
|
||||||
static const identityCreator = '$settings/identity_creator';
|
static const identityCreator = '$settings/identity_creator';
|
||||||
static const rulesFilterCreator = '$settings/rules_filter_creator';
|
static const rulesFilterCreator = '$settings/rules_filter_creator';
|
||||||
|
|||||||
Reference in New Issue
Block a user