Skip to content

Commit

Permalink
Tw 2046 group name validation (#2119)
Browse files Browse the repository at this point in the history
  • Loading branch information
KhaledNjim authored Nov 20, 2024
1 parent e2dc5da commit 91e7e6a
Show file tree
Hide file tree
Showing 19 changed files with 460 additions and 71 deletions.
3 changes: 2 additions & 1 deletion assets/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3098,5 +3098,6 @@
"logoutDialogWarning": "You will lose access to encrypted messages. We recommend that you enable chat backups before logging out",
"copyNumber": "Copy number",
"callViaCarrier": "Call via Carrier",
"scanQrCodeToJoin": "Installation of the mobile application will allow you to contact people from your phone's address book, your chats will be synchronised between devices"
"scanQrCodeToJoin": "Installation of the mobile application will allow you to contact people from your phone's address book, your chats will be synchronised between devices",
"thisFieldCannotBeBlank": "This field cannot be blank"
}
5 changes: 5 additions & 0 deletions lib/di/global/get_it_initializer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import 'package:fluffychat/domain/usecase/search/search_recent_chat_interactor.d
import 'package:fluffychat/domain/usecase/search/server_search_interactor.dart';
import 'package:fluffychat/domain/usecase/settings/save_language_interactor.dart';
import 'package:fluffychat/domain/usecase/settings/update_profile_interactor.dart';
import 'package:fluffychat/domain/usecase/verify_name_interactor.dart';
import 'package:fluffychat/event/twake_event_dispatcher.dart';
import 'package:fluffychat/pages/chat/chat_pinned_events/pinned_events_controller.dart';
import 'package:fluffychat/utils/famedlysdk_store.dart';
Expand Down Expand Up @@ -344,6 +345,10 @@ class GetItInitializer {
getIt.registerSingleton<DownloadMediaFileInteractor>(
DownloadMediaFileInteractor(),
);

getIt.registerFactory<VerifyNameInteractor>(
() => VerifyNameInteractor(),
);
}

void _bindingControllers() {
Expand Down
8 changes: 8 additions & 0 deletions lib/domain/app_state/validator/verify_name_view_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:fluffychat/presentation/state/failure.dart';
import 'package:fluffychat/presentation/state/success.dart';

class VerifyNameSuccessViewState extends UIState {}

class VerifyNameFailure extends FeatureFailure {
const VerifyNameFailure(dynamic exception) : super(exception: exception);
}
21 changes: 21 additions & 0 deletions lib/domain/exception/verify_name_exception.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:equatable/equatable.dart';

abstract class VerifyNameException extends Equatable implements Exception {
static const nameWithOnlySpace = 'The name cannot contain only spaces';
static const emptyName = 'The name cannot be empty';

final String? message;
const VerifyNameException(this.message);

@override
List<Object?> get props => [message];
}

class NameWithSpaceOnlyException extends VerifyNameException {
const NameWithSpaceOnlyException()
: super(VerifyNameException.nameWithOnlySpace);
}

class EmptyNameException extends VerifyNameException {
const EmptyNameException() : super(VerifyNameException.emptyName);
}
20 changes: 20 additions & 0 deletions lib/domain/model/extensions/list_validator_extension.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'package:dartz/dartz.dart';
import 'package:fluffychat/app_state/failure.dart';
import 'package:fluffychat/app_state/success.dart';
import 'package:fluffychat/domain/app_state/validator/verify_name_view_state.dart';
import 'package:fluffychat/domain/model/verification/new_name_request.dart';
import 'package:fluffychat/domain/model/verification/validator.dart';

extension ListValidatorExtension on List<Validator> {
Either<Failure, Success> getValidatorNameViewState(
NewNameRequest newNameRequest,
) {
for (final validator in this) {
final either = validator.validate(newNameRequest);
if (either.isLeft()) {
return either;
}
}
return Right<Failure, Success>(VerifyNameSuccessViewState());
}
}
15 changes: 15 additions & 0 deletions lib/domain/model/extensions/validator_failure_extension.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'package:fluffychat/domain/app_state/validator/verify_name_view_state.dart';
import 'package:fluffychat/domain/exception/verify_name_exception.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';

extension ValidatorFailureExtension on VerifyNameFailure {
String getMessage(BuildContext context) {
if (exception is NameWithSpaceOnlyException ||
exception is EmptyNameException) {
return L10n.of(context)!.thisFieldCannotBeBlank;
} else {
return '';
}
}
}
20 changes: 20 additions & 0 deletions lib/domain/model/verification/composite_name_validator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'package:dartz/dartz.dart';
import 'package:fluffychat/app_state/failure.dart';
import 'package:fluffychat/app_state/success.dart';
import 'package:fluffychat/domain/app_state/validator/verify_name_view_state.dart';
import 'package:fluffychat/domain/model/verification/new_name_request.dart';
import 'package:fluffychat/domain/model/verification/validator.dart';
import 'package:fluffychat/domain/model/extensions/list_validator_extension.dart';

class CompositeNameValidator extends Validator<NewNameRequest> {
final List<Validator> _listValidator;

CompositeNameValidator(this._listValidator);

@override
Either<Failure, Success> validate(NewNameRequest value) {
return _listValidator.isNotEmpty
? _listValidator.getValidatorNameViewState(value)
: Right<Failure, Success>(VerifyNameSuccessViewState());
}
}
20 changes: 20 additions & 0 deletions lib/domain/model/verification/empty_name_validator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'package:dartz/dartz.dart';
import 'package:fluffychat/app_state/failure.dart';
import 'package:fluffychat/app_state/success.dart';
import 'package:fluffychat/domain/app_state/validator/verify_name_view_state.dart';
import 'package:fluffychat/domain/exception/verify_name_exception.dart';
import 'package:fluffychat/domain/model/verification/new_name_request.dart';
import 'package:fluffychat/domain/model/verification/validator.dart';

class EmptyNameValidator extends Validator<NewNameRequest> {
@override
Either<Failure, Success> validate(NewNameRequest value) {
if (value.value == null || value.value!.isEmpty) {
return const Left<Failure, Success>(
VerifyNameFailure(EmptyNameException()),
);
} else {
return Right<Failure, Success>(VerifyNameSuccessViewState());
}
}
}
22 changes: 22 additions & 0 deletions lib/domain/model/verification/name_with_space_only_validator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:dartz/dartz.dart';
import 'package:fluffychat/app_state/failure.dart';
import 'package:fluffychat/app_state/success.dart';
import 'package:fluffychat/domain/app_state/validator/verify_name_view_state.dart';
import 'package:fluffychat/domain/exception/verify_name_exception.dart';
import 'package:fluffychat/domain/model/verification/new_name_request.dart';
import 'package:fluffychat/domain/model/verification/validator.dart';

class NameWithSpaceOnlyValidator extends Validator<NewNameRequest> {
@override
Either<Failure, Success> validate(NewNameRequest value) {
if (value.value != null &&
value.value!.isNotEmpty &&
value.value!.trim().isEmpty) {
return const Left<Failure, Success>(
VerifyNameFailure(NameWithSpaceOnlyException()),
);
} else {
return Right<Failure, Success>(VerifyNameSuccessViewState());
}
}
}
10 changes: 10 additions & 0 deletions lib/domain/model/verification/new_name_request.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:equatable/equatable.dart';

class NewNameRequest with EquatableMixin {
final String? value;

NewNameRequest(this.value);

@override
List<Object?> get props => [value];
}
7 changes: 7 additions & 0 deletions lib/domain/model/verification/validator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:dartz/dartz.dart';
import 'package:fluffychat/app_state/failure.dart';
import 'package:fluffychat/app_state/success.dart';

abstract class Validator<T> {
Either<Failure, Success> validate(T value);
}
21 changes: 21 additions & 0 deletions lib/domain/usecase/verify_name_interactor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:dartz/dartz.dart';
import 'package:fluffychat/app_state/failure.dart';
import 'package:fluffychat/app_state/success.dart';
import 'package:fluffychat/domain/app_state/validator/verify_name_view_state.dart';
import 'package:fluffychat/domain/model/verification/composite_name_validator.dart';
import 'package:fluffychat/domain/model/verification/new_name_request.dart';
import 'package:fluffychat/domain/model/verification/validator.dart';

class VerifyNameInteractor {
Either<Failure, Success> execute(
String? newName,
List<Validator> listValidator,
) {
try {
return CompositeNameValidator(listValidator)
.validate(NewNameRequest(newName));
} catch (exception) {
return Left<Failure, Success>(VerifyNameFailure(exception));
}
}
}
26 changes: 25 additions & 1 deletion lib/pages/chat_details/chat_details_edit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import 'package:fluffychat/di/global/get_it_initializer.dart';
import 'package:fluffychat/domain/app_state/room/update_group_chat_failure.dart';
import 'package:fluffychat/domain/app_state/room/update_group_chat_success.dart';
import 'package:fluffychat/domain/app_state/room/upload_content_state.dart';
import 'package:fluffychat/domain/app_state/validator/verify_name_view_state.dart';
import 'package:fluffychat/domain/model/extensions/validator_failure_extension.dart';
import 'package:fluffychat/domain/model/verification/empty_name_validator.dart';
import 'package:fluffychat/domain/model/verification/name_with_space_only_validator.dart';
import 'package:fluffychat/domain/usecase/room/update_group_chat_interactor.dart';
import 'package:fluffychat/domain/usecase/room/upload_content_for_web_interactor.dart';
import 'package:fluffychat/domain/usecase/room/upload_content_interactor.dart';
import 'package:fluffychat/domain/usecase/verify_name_interactor.dart';
import 'package:fluffychat/pages/chat_details/chat_details_edit_context_menu_actions.dart';
import 'package:fluffychat/pages/chat_details/chat_details_edit_view.dart';
import 'package:fluffychat/pages/chat_details/chat_details_edit_view_style.dart';
Expand Down Expand Up @@ -54,6 +59,7 @@ class ChatDetailsEditController extends State<ChatDetailsEdit>
final uploadContentInteractor = getIt.get<UploadContentInteractor>();
final uploadContentWebInteractor =
getIt.get<UploadContentInBytesInteractor>();
final verifyNameInteractor = getIt.get<VerifyNameInteractor>();

Room? room;

Expand All @@ -73,6 +79,7 @@ class ChatDetailsEditController extends State<ChatDetailsEdit>
final MenuController menuController = MenuController();

final isEditedGroupInfoNotifier = ValueNotifier<bool>(false);
final isValidGroupNameNotifier = ValueNotifier<bool>(false);

Client get client => Matrix.of(context).client;

Expand Down Expand Up @@ -443,6 +450,22 @@ class ChatDetailsEditController extends State<ChatDetailsEdit>
avatarAssetEntity = null;
}

String? getErrorMessage(String content) {
if (content == room?.name) {
return null;
}
final result = verifyNameInteractor.execute(
content,
[EmptyNameValidator(), NameWithSpaceOnlyValidator()],
);

return result.fold(
(failure) =>
failure is VerifyNameFailure ? failure.getMessage(context) : null,
(success) => null,
);
}

@override
void dispose() {
_clearImageInMemory();
Expand All @@ -463,11 +486,12 @@ class ChatDetailsEditController extends State<ChatDetailsEdit>
if (_isEditAvatar) {
return;
}

isEditedGroupInfoNotifier.value =
groupNameTextEditingController.text != room?.name;
groupNameEmptyNotifier.value =
groupNameTextEditingController.text.isEmpty;
isValidGroupNameNotifier.value =
getErrorMessage(groupNameTextEditingController.text) == null;
});
}

Expand Down
113 changes: 63 additions & 50 deletions lib/pages/chat_details/chat_details_edit_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,28 @@ class ChatDetailsEditView extends StatelessWidget {
),
const Spacer(),
ValueListenableBuilder(
valueListenable: controller.isEditedGroupInfoNotifier,
builder: (context, value, child) {
if (!value) {
return const SizedBox.shrink();
}
return child!;
valueListenable: controller.isValidGroupNameNotifier,
builder: (context, isValid, child) {
return ValueListenableBuilder(
valueListenable: controller.isEditedGroupInfoNotifier,
builder: (context, value, child) {
if (!value || !isValid) {
return const SizedBox.shrink();
}
return child!;
},
child: Padding(
padding: ChatDetailEditViewStyle.doneIconPadding,
child: IconButton(
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
splashColor: Colors.transparent,
onPressed: () => controller.handleSaveAction(context),
icon: const Icon(Icons.done),
),
),
);
},
child: Padding(
padding: ChatDetailEditViewStyle.doneIconPadding,
child: IconButton(
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
splashColor: Colors.transparent,
onPressed: () => controller.handleSaveAction(context),
icon: const Icon(Icons.done),
),
),
),
],
),
Expand Down Expand Up @@ -323,40 +328,48 @@ class _GroupNameField extends StatelessWidget {
Widget build(BuildContext context) {
return Padding(
padding: ChatDetailEditViewStyle.textFieldPadding,
child: TextField(
style: ChatDetailEditViewStyle.textFieldStyle(context),
controller: controller.groupNameTextEditingController,
contextMenuBuilder: mobileTwakeContextMenuBuilder,
focusNode: controller.groupNameFocusNode,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: Theme.of(context).colorScheme.shadow),
),
labelText: L10n.of(context)!.widgetName,
labelStyle: ChatDetailEditViewStyle.textFieldLabelStyle(context),
hintText: L10n.of(context)!.enterGroupName,
hintStyle: ChatDetailEditViewStyle.textFieldHintStyle(context),
contentPadding: ChatDetailEditViewStyle.contentPadding,
suffixIcon: ValueListenableBuilder<bool>(
valueListenable: controller.groupNameEmptyNotifier,
builder: (context, isGroupNameEmpty, child) {
if (isGroupNameEmpty) {
return child!;
}

return IconButton(
onPressed: () =>
controller.groupNameTextEditingController.clear(),
icon: Icon(
Icons.cancel_outlined,
size: ChatDetailEditViewStyle.clearIconSize,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
);
},
child: const SizedBox.shrink(),
),
),
child: ValueListenableBuilder(
valueListenable: controller.isValidGroupNameNotifier,
builder: (context, value, _) {
return TextField(
style: ChatDetailEditViewStyle.textFieldStyle(context),
controller: controller.groupNameTextEditingController,
contextMenuBuilder: mobileTwakeContextMenuBuilder,
focusNode: controller.groupNameFocusNode,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide:
BorderSide(color: Theme.of(context).colorScheme.shadow),
),
labelText: L10n.of(context)!.widgetName,
labelStyle: ChatDetailEditViewStyle.textFieldLabelStyle(context),
hintText: L10n.of(context)!.enterGroupName,
hintStyle: ChatDetailEditViewStyle.textFieldHintStyle(context),
contentPadding: ChatDetailEditViewStyle.contentPadding,
errorText: controller.getErrorMessage(
controller.groupNameTextEditingController.text,
),
suffixIcon: ValueListenableBuilder<bool>(
valueListenable: controller.groupNameEmptyNotifier,
builder: (context, isGroupNameEmpty, child) {
if (isGroupNameEmpty) {
return child!;
}
return IconButton(
onPressed: () =>
controller.groupNameTextEditingController.clear(),
icon: Icon(
Icons.cancel_outlined,
size: ChatDetailEditViewStyle.clearIconSize,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
);
},
child: const SizedBox.shrink(),
),
),
);
},
),
);
}
Expand Down
Loading

0 comments on commit 91e7e6a

Please sign in to comment.