TF-524 Create RecentSearchCache and TypeAHeadQuickSearch widget
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:tmail_ui_user/features/caching/config/hive_cache_client.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/recent_search_cache.dart';
|
||||
|
||||
class RecentSearchCacheClient extends HiveCacheClient<RecentSearchCache> {
|
||||
|
||||
@override
|
||||
String get tableName => 'RecentSearchCache';
|
||||
|
||||
@override
|
||||
Future<void> clearAllData() {
|
||||
return Future.sync(() async {
|
||||
final boxState = await openBox();
|
||||
boxState.clear();
|
||||
}).catchError((error) {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteItem(String key) {
|
||||
return Future.sync(() async {
|
||||
final boxState = await openBox();
|
||||
return boxState.delete(key);
|
||||
}).catchError((error) {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteMultipleItem(List<String> listKey) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<RecentSearchCache>> getAll() {
|
||||
return Future.sync(() async {
|
||||
final boxState = await openBox();
|
||||
return boxState.values.toList();
|
||||
}).catchError((error) {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RecentSearchCache?> getItem(String key) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> insertItem(String key, RecentSearchCache newObject) {
|
||||
return Future.sync(() async {
|
||||
final boxState = await openBox();
|
||||
boxState.put(key, newObject);
|
||||
}).catchError((error) {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> insertMultipleItem(Map<String, RecentSearchCache> mapObject) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isExistItem(String key) {
|
||||
return Future.sync(() async {
|
||||
final boxState = await openBox();
|
||||
return boxState.containsKey(key);
|
||||
}).catchError((error) {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isExistTable() {
|
||||
return Future.sync(() async {
|
||||
return await Hive.boxExists(tableName);
|
||||
}).catchError((error) {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Box<RecentSearchCache>> openBox() {
|
||||
return Future.sync(() async {
|
||||
if (Hive.isBoxOpen(tableName)) {
|
||||
return Hive.box<RecentSearchCache>(tableName);
|
||||
}
|
||||
return await Hive.openBox<RecentSearchCache>(tableName);
|
||||
}).catchError((error) {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateItem(String key, RecentSearchCache newObject) {
|
||||
return Future.sync(() async {
|
||||
final boxState = await openBox();
|
||||
boxState.put(key, newObject);
|
||||
}).catchError((error) {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateMultipleItem(Map<String, RecentSearchCache> mapObject) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,5 @@ class CachingConstants {
|
||||
static const int STATE_TYPE_IDENTIFY = 4;
|
||||
static const int EMAIL_CACHE_IDENTIFY = 5;
|
||||
static const int EMAIL_ADDRESS_HIVE_CACHE_IDENTIFY = 6;
|
||||
static const int RECENT_SEARCH_HIVE_CACHE_IDENTIFY = 7;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
|
||||
import 'package:core/core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
|
||||
typedef OnNewSearchQuery = Function(String);
|
||||
|
||||
class SearchFormWidgetBuilder {
|
||||
|
||||
final _typeAheadController = TextEditingController();
|
||||
final BuildContext _context;
|
||||
final ImagePaths _imagePaths;
|
||||
|
||||
OnNewSearchQuery? _onNewSearchQuery;
|
||||
|
||||
SearchFormWidgetBuilder(this._context, this._imagePaths);
|
||||
|
||||
SearchFormWidgetBuilder onNewSearchQuery(OnNewSearchQuery onNewSearchQuery) {
|
||||
_onNewSearchQuery = onNewSearchQuery;
|
||||
return this;
|
||||
}
|
||||
|
||||
Widget build() {
|
||||
return Container(
|
||||
key: const Key('search_folder_form'),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColor.searchBorderColor),
|
||||
color: AppColor.primaryLightColor),
|
||||
child: TypeAheadFormField(
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
controller: _typeAheadController,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.only(left: 0, top: 15, bottom: 15, right: 15),
|
||||
hintText: AppLocalizations.of(_context).search_folder,
|
||||
hintStyle: const TextStyle(color: AppColor.searchHintTextColor, fontSize: 15.0, fontWeight: FontWeight.w500),
|
||||
icon: Padding(
|
||||
padding: const EdgeInsets.only(left: 20),
|
||||
child: SvgPicture.asset(_imagePaths.icSearch, width: 24, height: 24, fit: BoxFit.fill),
|
||||
))),
|
||||
debounceDuration: const Duration(milliseconds: 300),
|
||||
suggestionsCallback: (pattern) async {
|
||||
if (_onNewSearchQuery != null) {
|
||||
_onNewSearchQuery!(pattern);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
itemBuilder: (BuildContext context, itemData) => const SizedBox.shrink(),
|
||||
onSuggestionSelected: (suggestion) {},
|
||||
noItemsFoundBuilder: (context) => const SizedBox(),
|
||||
hideOnEmpty: true,
|
||||
hideOnError: true,
|
||||
hideOnLoading: true,
|
||||
hideSuggestionsOnKeyboardHide: true,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart';
|
||||
|
||||
part 'recent_search_cache.g.dart';
|
||||
|
||||
@HiveType(typeId: CachingConstants.RECENT_SEARCH_HIVE_CACHE_IDENTIFY)
|
||||
class RecentSearchCache extends HiveObject with EquatableMixin {
|
||||
|
||||
@HiveField(0)
|
||||
final String value;
|
||||
|
||||
@HiveField(1)
|
||||
final DateTime creationTime;
|
||||
|
||||
RecentSearchCache(this.value, this.creationTime);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [value, creationTime];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class RecentSearch with EquatableMixin {
|
||||
final String value;
|
||||
final DateTime creationDate;
|
||||
|
||||
RecentSearch(this.value, this.creationDate);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [value, creationDate];
|
||||
}
|
||||
@@ -1,21 +1,24 @@
|
||||
import 'package:core/core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:model/model.dart';
|
||||
import 'package:tmail_ui_user/features/base/mixin/network_connection_mixin.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/composer_view_web.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/email_view.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_view_web.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/filter_email_popup_menu_mixin.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/email_quick_search_item_tile_widget.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/recent_search_item_tile_widget.dart';
|
||||
import 'package:tmail_ui_user/features/manage_account/presentation/model/app_setting.dart';
|
||||
import 'package:tmail_ui_user/features/manage_account/presentation/model/reading_pane.dart';
|
||||
import 'package:tmail_ui_user/features/thread/presentation/extensions/filter_message_option_extension.dart';
|
||||
import 'package:tmail_ui_user/features/thread/presentation/thread_view.dart';
|
||||
import 'package:tmail_ui_user/features/thread/presentation/widgets/search_app_bar_widget.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
import 'package:tmail_ui_user/main/routes/app_routes.dart';
|
||||
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
|
||||
@@ -157,7 +160,9 @@ class MailboxDashBoardView extends GetWidget<MailboxDashBoardController> with Ne
|
||||
}),
|
||||
const SizedBox(width: 16),
|
||||
Obx(() => !controller.isSearchActive() ? const Spacer() : const SizedBox.shrink()),
|
||||
Obx(() => controller.isSearchActive() ? Expanded(child: _buildSearchForm(context)) : const SizedBox.shrink()),
|
||||
Obx(() => controller.isSearchActive()
|
||||
? Expanded(child: _buildSearchForm(context))
|
||||
: const SizedBox.shrink()),
|
||||
Obx(() => !controller.isSearchActive()
|
||||
? (SearchBarView(_imagePaths)
|
||||
..hintTextSearch(AppLocalizations.of(context).search_emails)
|
||||
@@ -396,24 +401,119 @@ class MailboxDashBoardView extends GetWidget<MailboxDashBoardController> with Ne
|
||||
}
|
||||
|
||||
Widget _buildSearchForm(BuildContext context) {
|
||||
return (SearchAppBarWidget(context, _imagePaths, _responsiveUtils,
|
||||
controller.searchQuery,
|
||||
controller.searchFocus,
|
||||
controller.searchInputController,
|
||||
suggestionSearch: controller.suggestionSearch)
|
||||
..addDecoration(const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
color: AppColor.colorBgSearchBar))
|
||||
..setMargin(const EdgeInsets.only(right: 10))
|
||||
..setHeightSearchBar(45)
|
||||
..setHintText(AppLocalizations.of(context).search_mail)
|
||||
..addOnCancelSearchPressed(() {
|
||||
controller.disableSearch();
|
||||
controller.dispatchAction(CancelSelectionAllEmailAction());
|
||||
})
|
||||
..addOnClearTextSearchAction(() => controller.clearSearchText())
|
||||
..addOnTextChangeSearchAction((query) => controller.addSuggestionSearch(query))
|
||||
..addOnSearchTextAction((query) => controller.searchEmail(context, query)))
|
||||
.build();
|
||||
return Row(
|
||||
children: [
|
||||
buildIconWeb(
|
||||
icon: SvgPicture.asset(_imagePaths.icBack, color: AppColor.colorTextButton, fit: BoxFit.fill),
|
||||
onTap: () {
|
||||
controller.disableSearch();
|
||||
controller.dispatchAction(CancelSelectionAllEmailAction());
|
||||
}),
|
||||
Expanded(child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
color: AppColor.colorBgSearchBar),
|
||||
height: 45,
|
||||
child: TypeAheadFormFieldQuickSearch<PresentationEmail, RecentSearch>(
|
||||
textFieldConfiguration: QuickSearchTextFieldConfiguration(
|
||||
controller: controller.searchInputController,
|
||||
autofocus: true,
|
||||
focusNode: controller.searchFocus,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: AppLocalizations.of(context).search_mail,
|
||||
hintStyle: const TextStyle(color: AppColor.colorHintSearchBar, fontSize: 17.0),
|
||||
labelStyle: const TextStyle(color: AppColor.colorHintSearchBar, fontSize: 17.0)
|
||||
),
|
||||
leftButton: buildIconWeb(
|
||||
icon: SvgPicture.asset(_imagePaths.icSearchBar, width: 16, height: 16, fit: BoxFit.fill),
|
||||
onTap: () => {}),
|
||||
rightButton: buildIconWeb(
|
||||
icon: SvgPicture.asset(_imagePaths.icComposerClose, width: 18, height: 18, fit: BoxFit.fill),
|
||||
onTap: () {
|
||||
controller.searchInputController.clear();
|
||||
controller.clearSearchText();
|
||||
})
|
||||
),
|
||||
suggestionsBoxDecoration: QuickSearchSuggestionsBoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: _responsiveUtils.isDesktop(context) ? 556 : double.infinity,
|
||||
maxHeight: 322),
|
||||
),
|
||||
actionButton: Padding(
|
||||
padding: const EdgeInsets.only(left: 24, top: 24, right: 24, bottom: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
(ButtonBuilder(_imagePaths.icAttachmentSB)
|
||||
..decoration(BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: AppColor.colorButtonHeaderThread))
|
||||
..paddingIcon(const EdgeInsets.only(right: 5))
|
||||
..padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 8))
|
||||
..radiusSplash(10)
|
||||
..size(16)
|
||||
..textStyle(const TextStyle(fontSize: 13, color: AppColor.colorTextButtonHeaderThread))
|
||||
..onPressActionClick(() => {})
|
||||
..text(AppLocalizations.of(context).hasAttachment, isVertical: false))
|
||||
.build(),
|
||||
const SizedBox(width: 8),
|
||||
(ButtonBuilder(_imagePaths.icCalendarSB)
|
||||
..decoration(BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: AppColor.colorButtonHeaderThread))
|
||||
..paddingIcon(const EdgeInsets.only(right: 5))
|
||||
..padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 8))
|
||||
..radiusSplash(10)
|
||||
..size(16)
|
||||
..textStyle(const TextStyle(fontSize: 13, color: AppColor.colorTextButtonHeaderThread))
|
||||
..onPressActionClick(() => {})
|
||||
..text(AppLocalizations.of(context).last7Days, isVertical: false))
|
||||
.build(),
|
||||
const SizedBox(width: 8),
|
||||
(ButtonBuilder(_imagePaths.icUserSB)
|
||||
..decoration(BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: AppColor.colorButtonHeaderThread))
|
||||
..paddingIcon(const EdgeInsets.only(right: 5))
|
||||
..padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 8))
|
||||
..radiusSplash(10)
|
||||
..size(16)
|
||||
..textStyle(const TextStyle(fontSize: 13, color: AppColor.colorTextButtonHeaderThread))
|
||||
..onPressActionClick(() => {})
|
||||
..text(AppLocalizations.of(context).fromMe, isVertical: false))
|
||||
.build(),
|
||||
],
|
||||
),
|
||||
),
|
||||
titleHeaderRecent: Padding(
|
||||
padding: const EdgeInsets.only(left: 24, right: 24, bottom: 8),
|
||||
child: Text(AppLocalizations.of(context).recent,
|
||||
style: const TextStyle(fontSize: 13.0,
|
||||
color: AppColor.colorTextButtonHeaderThread,
|
||||
fontWeight: FontWeight.w500))),
|
||||
getRecentCallback: (pattern) async {
|
||||
return [];
|
||||
},
|
||||
itemRecentBuilder: (context, recent) {
|
||||
return RecentSearchItemTileWidget(recent);
|
||||
},
|
||||
suggestionsCallback: (pattern) async {
|
||||
return [];
|
||||
},
|
||||
itemBuilder: (context, email) {
|
||||
return EmailQuickSearchItemTileWidget(email, controller.selectedMailbox.value);
|
||||
},
|
||||
onSuggestionSelected: (autoCompleteResult) async {
|
||||
}),
|
||||
)),
|
||||
const SizedBox(width: 16),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
|
||||
import 'package:core/core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:model/email/presentation_email.dart';
|
||||
import 'package:model/extensions/presentation_email_extension.dart';
|
||||
import 'package:model/mailbox/presentation_mailbox.dart';
|
||||
|
||||
class EmailQuickSearchItemTileWidget extends StatelessWidget {
|
||||
|
||||
final imagePath = Get.find<ImagePaths>();
|
||||
|
||||
final PresentationEmail _presentationEmail;
|
||||
final PresentationMailbox? _presentationMailbox;
|
||||
|
||||
EmailQuickSearchItemTileWidget(
|
||||
this._presentationEmail,
|
||||
this._presentationMailbox,
|
||||
{Key? key}
|
||||
) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
_presentationEmail.hasStarred ? imagePath.icStar : imagePath.icUnStar,
|
||||
width: 20, height: 20, fit: BoxFit.fill),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Text(_getInformationSender(),
|
||||
maxLines: 1,
|
||||
overflow: BuildUtils.isWeb ? null : TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(_presentationEmail.getEmailTitle(),
|
||||
maxLines: 1,
|
||||
overflow: BuildUtils.isWeb ? null : TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Colors.black)),
|
||||
),
|
||||
if (_presentationEmail.hasAttachment == true)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: SvgPicture.asset(imagePath.icAttachment, width: 14, height: 14, fit: BoxFit.fill),
|
||||
),
|
||||
Text(_presentationEmail.getReceivedAt(Localizations.localeOf(context).toLanguageTag()),
|
||||
textAlign: TextAlign.right,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Colors.black))
|
||||
]),
|
||||
const SizedBox(height: 3),
|
||||
Text(_presentationEmail.getPartialContent(),
|
||||
maxLines: 1,
|
||||
overflow: BuildUtils.isWeb ? null : TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: AppColor.colorContentEmail))
|
||||
]
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getInformationSender() {
|
||||
if (_presentationMailbox?.role == PresentationMailbox.roleSent
|
||||
|| _presentationMailbox?.role == PresentationMailbox.roleDrafts
|
||||
|| _presentationMailbox?.role == PresentationMailbox.roleOutbox) {
|
||||
return _presentationEmail.recipientsName();
|
||||
}
|
||||
return _presentationEmail.getSenderName();
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
|
||||
import 'package:core/core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart';
|
||||
|
||||
class RecentSearchItemTileWidget extends StatelessWidget {
|
||||
|
||||
final imagePath = Get.find<ImagePaths>();
|
||||
|
||||
final RecentSearch recentSearch;
|
||||
|
||||
RecentSearchItemTileWidget(this.recentSearch, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
SvgPicture.asset(imagePath.icClockSB),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(recentSearch.value,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Colors.black)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user