TF-48 Implement compose and send email with simple text
This commit is contained in:
@@ -1,9 +1,56 @@
|
||||
import 'package:core/core.dart';
|
||||
import 'package:core/presentation/utils/app_toast.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:html_editor_enhanced/html_editor.dart';
|
||||
import 'package:tmail_ui_user/features/composer/data/datasource/autocomplete_datasource.dart';
|
||||
import 'package:tmail_ui_user/features/composer/data/datasource/composer_datasource.dart';
|
||||
import 'package:tmail_ui_user/features/composer/data/datasource_impl/autocomplete_datasource_impl.dart';
|
||||
import 'package:tmail_ui_user/features/composer/data/datasource_impl/composer_datasource_impl.dart';
|
||||
import 'package:tmail_ui_user/features/composer/data/datasource_impl/local_autocomplete_datasource_impl.dart';
|
||||
import 'package:tmail_ui_user/features/composer/data/datasource_impl/local_composer_datasource_impl.dart';
|
||||
import 'package:tmail_ui_user/features/composer/data/local/email_address_database_manager.dart';
|
||||
import 'package:tmail_ui_user/features/composer/data/repository/auto_complete_repository_impl.dart';
|
||||
import 'package:tmail_ui_user/features/composer/data/repository/composer_repository_impl.dart';
|
||||
import 'package:tmail_ui_user/features/composer/domain/repository/auto_complete_repository.dart';
|
||||
import 'package:tmail_ui_user/features/composer/domain/repository/composer_repository.dart';
|
||||
import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_addresses_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/composer/domain/usecases/search_email_address_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ComposerBindings extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut(() => ComposerController());
|
||||
Get.lazyPut(() => EmailAddressDatabaseManager(Get.find<DatabaseClient>()));
|
||||
Get.lazyPut(() => ComposerDataSourceImpl());
|
||||
Get.lazyPut(() => LocalComposerDataSourceImpl(Get.find<EmailAddressDatabaseManager>()));
|
||||
Get.lazyPut<ComposerDataSource>(() => Get.find<ComposerDataSourceImpl>());
|
||||
Get.lazyPut(() => ComposerRepositoryImpl({
|
||||
DataSourceType.network: Get.find<ComposerDataSource>(),
|
||||
DataSourceType.local: Get.find<LocalComposerDataSourceImpl>(),
|
||||
}));
|
||||
Get.lazyPut<ComposerRepository>(() => Get.find<ComposerRepositoryImpl>());
|
||||
Get.lazyPut(() => SendEmailInteractor(Get.find<EmailRepository>()));
|
||||
Get.lazyPut(() => SaveEmailAddressesInteractor(Get.find<ComposerRepository>()));
|
||||
Get.lazyPut(() => AutoCompleteDataSourceImpl());
|
||||
Get.lazyPut<AutoCompleteDataSource>(() => Get.find<AutoCompleteDataSourceImpl>());
|
||||
Get.lazyPut(() => LocalAutoCompleteDataSourceImpl(Get.find<EmailAddressDatabaseManager>()));
|
||||
Get.lazyPut(() => AutoCompleteRepositoryImpl({
|
||||
DataSourceType.network: Get.find<AutoCompleteDataSource>(),
|
||||
DataSourceType.local: Get.find<LocalAutoCompleteDataSourceImpl>(),
|
||||
}));
|
||||
Get.lazyPut<AutoCompleteRepository>(() => Get.find<AutoCompleteRepositoryImpl>());
|
||||
Get.lazyPut(() => SearchEmailAddressInteractor(Get.find<AutoCompleteRepository>()));
|
||||
Get.lazyPut(() => Uuid());
|
||||
Get.lazyPut(() => HtmlEditorController());
|
||||
Get.lazyPut(() => ComposerController(
|
||||
Get.find<SendEmailInteractor>(),
|
||||
Get.find<SaveEmailAddressesInteractor>(),
|
||||
Get.find<SearchEmailAddressInteractor>(),
|
||||
Get.find<AppToast>(),
|
||||
Get.find<Uuid>(),
|
||||
Get.find<HtmlEditorController>()));
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,93 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:core/core.dart';
|
||||
import 'package:core/presentation/utils/app_toast.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:html_editor_enhanced/html_editor.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:jmap_dart_client/jmap/core/id.dart';
|
||||
import 'package:jmap_dart_client/jmap/core/session/session.dart';
|
||||
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
|
||||
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
||||
import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart';
|
||||
import 'package:jmap_dart_client/jmap/mail/email/email_body_value.dart';
|
||||
import 'package:model/model.dart';
|
||||
import 'package:tmail_ui_user/features/base/base_controller.dart';
|
||||
import 'package:tmail_ui_user/features/composer/domain/model/auto_complete_pattern.dart';
|
||||
import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart';
|
||||
import 'package:tmail_ui_user/features/composer/domain/state/search_email_address_state.dart';
|
||||
import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_addresses_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/composer/domain/usecases/search_email_address_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/constants/email_constants.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/model/attachment_file.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/extensions/email_content_extension.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/model/message_content.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ComposerController extends GetxController {
|
||||
class ComposerController extends BaseController {
|
||||
|
||||
final expandMode = ExpandMode.COLLAPSE.obs;
|
||||
final composerArguments = Rxn<ComposerArguments>();
|
||||
final isEnableEmailSendButton = false.obs;
|
||||
final listReplyToEmailAddress = <EmailAddress>[].obs;
|
||||
final htmlEditorController = HtmlEditorController();
|
||||
|
||||
final listEmailAddressSuggestion = [
|
||||
EmailAddress('Mike Jones', 'user1@linagora.com'),
|
||||
EmailAddress('Mike Smith', 'user2@linagora.com'),
|
||||
EmailAddress('Jane Smith', 'user3@linagora.com'),
|
||||
EmailAddress('User4', 'user4@linagora.com'),
|
||||
EmailAddress('User5', 'user5@linagora.com')
|
||||
];
|
||||
final SendEmailInteractor _sendEmailInteractor;
|
||||
final SaveEmailAddressesInteractor _saveEmailAddressInteractor;
|
||||
final SearchEmailAddressInteractor _searchEmailAddressInteractor;
|
||||
final AppToast _appToast;
|
||||
final Uuid _uuid;
|
||||
final HtmlEditorController htmlEditorController;
|
||||
|
||||
List<EmailAddress> listToEmailAddress = [];
|
||||
List<EmailAddress> listCcEmailAddress = [];
|
||||
List<EmailAddress> listBccEmailAddress = [];
|
||||
String? _subjectEmail;
|
||||
|
||||
ComposerController();
|
||||
void setSubjectEmail(String subject) => _subjectEmail = subject;
|
||||
|
||||
ComposerController(
|
||||
this._sendEmailInteractor,
|
||||
this._saveEmailAddressInteractor,
|
||||
this._searchEmailAddressInteractor,
|
||||
this._appToast,
|
||||
this._uuid,
|
||||
this.htmlEditorController,
|
||||
);
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
void onReady() async {
|
||||
super.onReady();
|
||||
_getSelectedEmail();
|
||||
await searchEmailAddressSuggestion('');
|
||||
}
|
||||
|
||||
@override
|
||||
void onDone() {
|
||||
viewState.value.fold(
|
||||
(failure) {
|
||||
_appToast.showErrorToast(AppLocalizations.of(Get.context!).error_message_sent);
|
||||
Get.back();
|
||||
},
|
||||
(success) {
|
||||
_saveEmailAddress();
|
||||
_appToast.showSuccessToast(AppLocalizations.of(Get.context!).message_sent);
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(error) {
|
||||
_appToast.showErrorToast(AppLocalizations.of(Get.context!).error_message_sent);
|
||||
Get.back();
|
||||
}
|
||||
|
||||
void _getSelectedEmail() {
|
||||
final arguments = Get.arguments;
|
||||
if (arguments is ComposerArguments) {
|
||||
@@ -166,6 +213,67 @@ class ComposerController extends GetxController {
|
||||
return emailQuotedHtml;
|
||||
}
|
||||
|
||||
Future<Email> generateEmail() async {
|
||||
final generateEmailId = EmailId(Id(_uuid.v1()));
|
||||
final outboxMailboxId = composerArguments.value!.mapMailboxId[PresentationMailbox.roleOutbox];
|
||||
final listFromEmailAddress = {
|
||||
EmailAddress(
|
||||
composerArguments.value!.userProfile.getNameDisplay(),
|
||||
composerArguments.value!.userProfile.getEmailAddress())
|
||||
};
|
||||
final generatePartId = PartId(_uuid.v1());
|
||||
final generateBlobId = Id(_uuid.v1());
|
||||
|
||||
final emailBodyText = await htmlEditorController.getText();
|
||||
|
||||
return Email(
|
||||
generateEmailId,
|
||||
mailboxIds: {outboxMailboxId!: true},
|
||||
from: listFromEmailAddress,
|
||||
to: listToEmailAddress.toSet(),
|
||||
cc: listCcEmailAddress.toSet(),
|
||||
bcc: listBccEmailAddress.toSet(),
|
||||
subject: _subjectEmail,
|
||||
htmlBody: {
|
||||
EmailBodyPart(
|
||||
partId: generatePartId,
|
||||
blobId: generateBlobId,
|
||||
type: MediaType.parse(EmailConstants.HTML_TEXT)
|
||||
)},
|
||||
bodyValues: {
|
||||
generatePartId: EmailBodyValue(emailBodyText, false, false)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void sendEmailAction(BuildContext context) async {
|
||||
if (isEnableEmailSendButton.value) {
|
||||
_appToast.showToast(AppLocalizations.of(context).your_email_being_sent);
|
||||
|
||||
final email = await generateEmail();
|
||||
final accountId = composerArguments.value!.session.accounts.keys.first;
|
||||
final sentMailboxId = composerArguments.value!.mapMailboxId[PresentationMailbox.roleSent];
|
||||
final submissionCreateId = Id(_uuid.v1());
|
||||
|
||||
consumeState(_sendEmailInteractor.execute(accountId, EmailRequest(email, submissionCreateId, mailboxIdSaved: sentMailboxId)));
|
||||
} else {
|
||||
_appToast.showErrorToast(AppLocalizations.of(context).your_email_should_have_at_least_one_recipient);
|
||||
}
|
||||
}
|
||||
|
||||
void _saveEmailAddress() {
|
||||
final listEmailAddressCanSave = Set<EmailAddress>();
|
||||
listEmailAddressCanSave.addAll(listToEmailAddress + listCcEmailAddress + listBccEmailAddress);
|
||||
_saveEmailAddressInteractor.execute(listEmailAddressCanSave.toList());
|
||||
}
|
||||
|
||||
Future<List<EmailAddress>> searchEmailAddressSuggestion(String word) async {
|
||||
return await _searchEmailAddressInteractor.execute(AutoCompletePattern(word: word))
|
||||
.then((value) => value.fold(
|
||||
(failure) => [],
|
||||
(success) => success is SearchEmailAddressSuccess ? success.listEmailAddress : []));
|
||||
}
|
||||
|
||||
void backToEmailViewAction() {
|
||||
Get.back();
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 24, top: 12, bottom: 12),
|
||||
child: Obx(() => (TopBarComposerWidgetBuilder(imagePaths, controller.isEnableEmailSendButton.value)
|
||||
..addSendEmailActionClick(() => controller.sendEmailAction(context))
|
||||
..addBackActionClick(() => controller.backToEmailViewAction()))
|
||||
.build()),
|
||||
);
|
||||
@@ -66,20 +67,7 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Obx(() {
|
||||
return (EmailAddressComposerWidgetBuilder(
|
||||
context,
|
||||
imagePaths,
|
||||
controller.expandMode.value,
|
||||
controller.listEmailAddressSuggestion,
|
||||
controller.listReplyToEmailAddress.isNotEmpty ? controller.listReplyToEmailAddress : controller.listToEmailAddress,
|
||||
controller.listCcEmailAddress,
|
||||
controller.listBccEmailAddress,
|
||||
controller.composerArguments.value?.userProfile)
|
||||
..addExpandAddressActionClick(() => controller.expandEmailAddressAction())
|
||||
..addOnUpdateListEmailAddressAction((prefixEmailAddress, listEmailAddress) => controller.updateListEmailAddress(prefixEmailAddress, listEmailAddress)))
|
||||
.build();
|
||||
})),
|
||||
child: _buildEmailAddress(context)),
|
||||
_buildSubjectEmail(context),
|
||||
Container(
|
||||
margin: EdgeInsets.zero,
|
||||
@@ -91,6 +79,22 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmailAddress(BuildContext context) {
|
||||
return Obx(() => (EmailAddressComposerWidgetBuilder(
|
||||
context,
|
||||
imagePaths,
|
||||
controller.expandMode.value,
|
||||
controller.listReplyToEmailAddress.isNotEmpty ? controller.listReplyToEmailAddress : controller.listToEmailAddress,
|
||||
controller.listCcEmailAddress,
|
||||
controller.listBccEmailAddress,
|
||||
controller.composerArguments.value?.userProfile)
|
||||
..addExpandAddressActionClick(() => controller.expandEmailAddressAction())
|
||||
..addOnUpdateListEmailAddressAction((prefixEmailAddress, listEmailAddress) => controller.updateListEmailAddress(prefixEmailAddress, listEmailAddress))
|
||||
..addOnSuggestionEmailAddress((word) => controller.searchEmailAddressSuggestion(word)))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubjectEmail(BuildContext context) {
|
||||
return Padding(
|
||||
@@ -99,6 +103,7 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
.key(Key('subject_email_input'))
|
||||
.textInputAction(TextInputAction.newline)
|
||||
.maxLines(null)
|
||||
.onChange((value) => controller.setSubjectEmail(value))
|
||||
.textStyle(TextStyle(color: AppColor.nameUserColor, fontSize: 14, fontWeight: FontWeight.w500))
|
||||
.textDecoration(InputDecoration(
|
||||
hintText: AppLocalizations.of(context).subject_email,
|
||||
@@ -118,7 +123,9 @@ class ComposerView extends GetWidget<ComposerController> {
|
||||
child: Container(
|
||||
margin: EdgeInsets.zero,
|
||||
color: AppColor.bgComposer,
|
||||
padding: EdgeInsets.only(bottom: 30),
|
||||
padding: controller.getEmailActionType() != EmailActionType.compose
|
||||
? EdgeInsets.only(bottom: 30, left: 16, right: 16)
|
||||
: EdgeInsets.only(bottom: 30),
|
||||
alignment: Alignment.topCenter,
|
||||
child: _buildEmailBodyEditorQuoted(context)
|
||||
)
|
||||
|
||||
+22
-12
@@ -1,16 +1,19 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:core/core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_chips_input/flutter_chips_input.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
||||
import 'package:model/model.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart';
|
||||
|
||||
typedef OnOpenExpandAddressActionClick = void Function();
|
||||
|
||||
typedef OnSuggestionEmailAddress = Future<List<EmailAddress>> Function(String word);
|
||||
typedef OnUpdateListEmailAddressAction = void Function(
|
||||
PrefixEmailAddress prefixEmailAddress,
|
||||
List<EmailAddress> listEmailAddress
|
||||
@@ -21,7 +24,6 @@ class EmailAddressComposerWidgetBuilder {
|
||||
final ImagePaths _imagePaths;
|
||||
final BuildContext _context;
|
||||
final ExpandMode _expandMode;
|
||||
final List<EmailAddress> _listEmailAddressSuggestion;
|
||||
final List<EmailAddress> _listToEmailAddress;
|
||||
final List<EmailAddress> _listCcEmailAddress;
|
||||
final List<EmailAddress> _listBccEmailAddress;
|
||||
@@ -31,12 +33,12 @@ class EmailAddressComposerWidgetBuilder {
|
||||
|
||||
OnOpenExpandAddressActionClick? _onOpenExpandAddressActionClick;
|
||||
OnUpdateListEmailAddressAction? _onUpdateListEmailAddressAction;
|
||||
OnSuggestionEmailAddress? _onSuggestionEmailAddress;
|
||||
|
||||
EmailAddressComposerWidgetBuilder(
|
||||
this._context,
|
||||
this._imagePaths,
|
||||
this._expandMode,
|
||||
this._listEmailAddressSuggestion,
|
||||
this._listToEmailAddress,
|
||||
this._listCcEmailAddress,
|
||||
this._listBccEmailAddress,
|
||||
@@ -51,6 +53,10 @@ class EmailAddressComposerWidgetBuilder {
|
||||
_onUpdateListEmailAddressAction = onUpdateListEmailAddressAction;
|
||||
}
|
||||
|
||||
void addOnSuggestionEmailAddress(OnSuggestionEmailAddress onSuggestionEmailAddress) {
|
||||
_onSuggestionEmailAddress = onSuggestionEmailAddress;
|
||||
}
|
||||
|
||||
Widget build() {
|
||||
keyToEmailAddress.currentState?.selectSuggestion(_listToEmailAddress);
|
||||
|
||||
@@ -115,29 +121,33 @@ class EmailAddressComposerWidgetBuilder {
|
||||
: Key('${prefixEmailAddress.toString()}_email_address_input'),
|
||||
initialValue: _getListEmailAddressCurrent(prefixEmailAddress),
|
||||
textStyle: TextStyle(color: AppColor.nameUserColor, fontSize: 14, fontWeight: FontWeight.w500),
|
||||
cursorColor: AppColor.primaryColor,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: AppLocalizations.of(_context).hint_text_email_address,
|
||||
hintMaxLines: 1,
|
||||
hintStyle: TextStyle(color: AppColor.baseTextColor, fontSize: 14, fontWeight: FontWeight.w500)),
|
||||
initialSuggestions: _listEmailAddressSuggestion,
|
||||
findSuggestions: (query) {
|
||||
if (query.isNotEmpty) {
|
||||
final lowercaseQuery = query.toLowerCase();
|
||||
return _listEmailAddressSuggestion
|
||||
.where((emailAddress) => emailAddress.getName().toLowerCase().contains(query.toLowerCase())
|
||||
|| emailAddress.getEmail().toLowerCase().contains(query.toLowerCase()))
|
||||
.toList(growable: false)
|
||||
..sort((email1, email2) => email1.asString().toLowerCase().indexOf(lowercaseQuery).compareTo(email2.asString().toLowerCase().indexOf(lowercaseQuery)));
|
||||
if (query.isNotEmpty && _onSuggestionEmailAddress != null) {
|
||||
return _onSuggestionEmailAddress!(query);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
onChanged: (data) {
|
||||
print('data: $data');
|
||||
if (_onUpdateListEmailAddressAction != null) {
|
||||
_onUpdateListEmailAddressAction!(prefixEmailAddress, data);
|
||||
}
|
||||
},
|
||||
onChipInputActionDone: (state, value) {
|
||||
if (GetUtils.isEmail(value)) {
|
||||
state.selectSuggestion(EmailAddress(value, value));
|
||||
}
|
||||
},
|
||||
onChipInputChangeFocusAction: (state, value) {
|
||||
if (GetUtils.isEmail(value)) {
|
||||
state.selectSuggestion(EmailAddress(value, value));
|
||||
}
|
||||
},
|
||||
chipBuilder: (context, state, emailAddress) {
|
||||
return InputChip(
|
||||
key: ObjectKey(emailAddress),
|
||||
|
||||
@@ -11,9 +11,9 @@ class TopBarComposerWidgetBuilder {
|
||||
OnSendEmailActionClick? _onSendEmailActionClick;
|
||||
|
||||
final ImagePaths _imagePaths;
|
||||
bool isEnableEmailSendButton;
|
||||
final bool _isEnableEmailSendButton;
|
||||
|
||||
TopBarComposerWidgetBuilder(this._imagePaths, this.isEnableEmailSendButton);
|
||||
TopBarComposerWidgetBuilder(this._imagePaths, this._isEnableEmailSendButton);
|
||||
|
||||
void addBackActionClick(OnBackActionClick onBackActionClick) {
|
||||
_onBackActionClick = onBackActionClick;
|
||||
@@ -68,12 +68,11 @@ class TopBarComposerWidgetBuilder {
|
||||
SizedBox(width: 10),
|
||||
ButtonBuilder(_imagePaths.icComposerSend)
|
||||
.key(Key('button_send_email'))
|
||||
.color(isEnableEmailSendButton ? AppColor.enableSendEmailButtonColor : AppColor.disableSendEmailButtonColor)
|
||||
.color(_isEnableEmailSendButton ? AppColor.enableSendEmailButtonColor : AppColor.disableSendEmailButtonColor)
|
||||
.onPressActionClick(() {
|
||||
if (_onSendEmailActionClick != null && isEnableEmailSendButton) {
|
||||
if (_onSendEmailActionClick != null && _isEnableEmailSendButton) {
|
||||
_onSendEmailActionClick!();
|
||||
}
|
||||
})
|
||||
}})
|
||||
.build(),
|
||||
SizedBox(width: 10),
|
||||
ButtonBuilder(_imagePaths.icComposerMenu).key(Key('button_menu_composer')).build(),
|
||||
|
||||
Reference in New Issue
Block a user