Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tw 2046 group name validation #2119

Merged
merged 7 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> {
KhaledNjim marked this conversation as resolved.
Show resolved Hide resolved
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());
}
}
}
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 &&
nqhhdev marked this conversation as resolved.
Show resolved Hide resolved
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 {
KhaledNjim marked this conversation as resolved.
Show resolved Hide resolved
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