Skip to content

Commit 993cdb4

Browse files
committed
TF-1302 Store and load recent emoji
1 parent cd692a4 commit 993cdb4

17 files changed

+336
-4
lines changed

lib/features/base/widget/emoji/emoji_button.dart

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:pointer_interceptor/pointer_interceptor.dart';
88
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
99

1010
typedef OnEmojiSelected = Function(String emoji);
11+
typedef OnRecentEmojiSelected = Future<Category?> Function();
1112

1213
class EmojiButton extends StatefulWidget {
1314
final EmojiData emojiData;
@@ -19,6 +20,7 @@ class EmojiButton extends StatefulWidget {
1920
final Color? iconColor;
2021
final String? iconTooltipMessage;
2122
final EdgeInsetsGeometry? iconPadding;
23+
final OnRecentEmojiSelected? onRecentEmojiSelected;
2224

2325
const EmojiButton({
2426
Key? key,
@@ -31,6 +33,7 @@ class EmojiButton extends StatefulWidget {
3133
this.iconColor,
3234
this.iconPadding,
3335
this.iconTooltipMessage,
36+
this.onRecentEmojiSelected,
3437
}) : super(key: key);
3538

3639
@override
@@ -45,6 +48,7 @@ class _EmojiButtonState extends State<EmojiButton>
4548
final GlobalKey _buttonKey = GlobalKey();
4649
OverlayEntry? _overlayEntry;
4750
bool _isDialogVisible = false;
51+
Future<Category?>? _recentEmoji;
4852

4953
late final AnimationController _animationController = AnimationController(
5054
vsync: this,
@@ -56,6 +60,17 @@ class _EmojiButtonState extends State<EmojiButton>
5660
late final Animation<double> _fadeAnimation =
5761
Tween<double>(begin: 0.0, end: 1.0).animate(_animationController);
5862

63+
Future<void> _loadRecentEmoji() async {
64+
if (widget.onRecentEmojiSelected != null) {
65+
final category = await widget.onRecentEmojiSelected!();
66+
if (category != null) {
67+
_recentEmoji = Future.value(category);
68+
return;
69+
}
70+
}
71+
_recentEmoji = Future.value(null);
72+
}
73+
5974
void _toggleEmojiDialog() {
6075
if (_isDialogVisible) {
6176
_closeDialog();
@@ -64,7 +79,7 @@ class _EmojiButtonState extends State<EmojiButton>
6479
}
6580
}
6681

67-
void _openDialog() {
82+
Future<void> _openDialog() async {
6883
widget.onPickerOpen();
6984

7085
if (!mounted || _isDialogVisible) return;
@@ -98,6 +113,8 @@ class _EmojiButtonState extends State<EmojiButton>
98113
}
99114
}
100115

116+
await _loadRecentEmoji();
117+
101118
_overlayEntry = OverlayEntry(
102119
builder: (context) {
103120
return PointerInterceptor(
@@ -191,6 +208,7 @@ class _EmojiButtonState extends State<EmojiButton>
191208
onEmojiSelected: (emojiId, emoji) {
192209
widget.onEmojiSelected(emoji);
193210
},
211+
recentEmoji: _recentEmoji,
194212
),
195213
),
196214
),
@@ -202,9 +220,11 @@ class _EmojiButtonState extends State<EmojiButton>
202220
},
203221
);
204222

205-
Overlay.maybeOf(context, rootOverlay: true)?.insert(_overlayEntry!);
223+
if (mounted) {
224+
Overlay.maybeOf(context, rootOverlay: true)?.insert(_overlayEntry!);
225+
}
206226
_animationController.forward(from: 0);
207-
setState(() => _isDialogVisible = true);
227+
if (mounted) setState(() => _isDialogVisible = true);
208228
}
209229

210230
Future<void> _closeDialog() async {
@@ -220,6 +240,7 @@ class _EmojiButtonState extends State<EmojiButton>
220240
_overlayEntry?.remove();
221241
_overlayEntry = null;
222242
_animationController.dispose();
243+
_recentEmoji = null;
223244
super.dispose();
224245
}
225246

lib/features/composer/presentation/composer_bindings.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_work
6262
import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_manager.dart';
6363
import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_worker_queue.dart';
6464
import 'package:tmail_ui_user/features/offline_mode/manager/sending_email_cache_manager.dart';
65+
import 'package:tmail_ui_user/features/reactions/presentation/reactions_interactor_bindings.dart';
6566
import 'package:tmail_ui_user/features/server_settings/domain/usecases/get_server_setting_interactor.dart';
6667
import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dart';
6768
import 'package:tmail_ui_user/features/upload/data/datasource/attachment_upload_datasource.dart';
@@ -309,6 +310,10 @@ class ComposerBindings extends BaseBindings {
309310
Get.find<ComposerRepository>(tag: composerId),
310311
Get.find<EmailRepository>(tag: composerId),
311312
), tag: composerId);
313+
314+
if (PlatformInfo.isWeb) {
315+
ReactionsInteractorBindings(composerId: composerId).dependencies();
316+
}
312317
}
313318

314319
@override
@@ -404,5 +409,9 @@ class ComposerBindings extends BaseBindings {
404409

405410
IdentityInteractorsBindings(composerId: composerId).dispose();
406411
PreferencesInteractorsBindings(composerId: composerId).dispose();
412+
413+
if (PlatformInfo.isWeb) {
414+
ReactionsInteractorBindings(composerId: composerId).dispose();
415+
}
407416
}
408417
}

lib/features/composer/presentation/composer_view_web.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,7 @@ class ComposerView extends GetWidget<ComposerController> {
537537
),
538538
Obx(() => BottomBarComposerWidget(
539539
imagePaths: controller.imagePaths,
540+
responsiveUtils: controller.responsiveUtils,
540541
isCodeViewEnabled: controller.richTextWebController!.codeViewEnabled,
541542
isFormattingOptionsEnabled: controller.richTextWebController!.isFormattingOptionsEnabled,
542543
hasReadReceipt: controller.hasRequestReadReceipt.value,
@@ -558,6 +559,7 @@ class ComposerView extends GetWidget<ComposerController> {
558559
onOpenInsertLink: controller.openInsertLink,
559560
onEmojiSelected: controller.insertEmojiToEditor,
560561
onPickerOpen: controller.handleOpenEmojiPicker,
562+
onRecentEmojiSelected: controller.getRecentReactions,
561563
)),
562564
],
563565
),
@@ -811,6 +813,7 @@ class ComposerView extends GetWidget<ComposerController> {
811813
),
812814
Obx(() => BottomBarComposerWidget(
813815
imagePaths: controller.imagePaths,
816+
responsiveUtils: controller.responsiveUtils,
814817
isCodeViewEnabled: controller.richTextWebController!.codeViewEnabled,
815818
isFormattingOptionsEnabled: controller.richTextWebController!.isFormattingOptionsEnabled,
816819
hasReadReceipt: controller.hasRequestReadReceipt.value,
@@ -832,6 +835,7 @@ class ComposerView extends GetWidget<ComposerController> {
832835
onOpenInsertLink: controller.openInsertLink,
833836
onEmojiSelected: controller.insertEmojiToEditor,
834837
onPickerOpen: controller.handleOpenEmojiPicker,
838+
onRecentEmojiSelected: controller.getRecentReactions,
835839
)),
836840
],
837841
),

lib/features/composer/presentation/extensions/handle_insert_emoji_to_editor_extension.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import 'package:core/utils/app_logger.dart';
2+
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
23
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
4+
import 'package:tmail_ui_user/features/reactions/domain/state/get_recent_reactions_state.dart';
5+
import 'package:tmail_ui_user/features/reactions/domain/usecase/get_recent_reactions_interactor.dart';
6+
import 'package:tmail_ui_user/features/reactions/domain/usecase/store_recent_reactions_interactor.dart';
7+
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
8+
import 'package:tmail_ui_user/main/utils/asset_manager.dart';
39

410
extension HandleInsertEmojiToEditorExtension on ComposerController {
511
void insertEmojiToEditor(String emoji) {
612
log('$runtimeType::insertEmojiToEditor: Emoji is $emoji');
713
richTextWebController?.insertEmoji(emoji);
14+
storeRecentReactions(emoji);
815
}
916

1017
void handleOpenEmojiPicker() {
@@ -13,4 +20,34 @@ extension HandleInsertEmojiToEditorExtension on ComposerController {
1320
clearFocusSubject();
1421
richTextWebController?.editorController.setFocus();
1522
}
23+
24+
void storeRecentReactions(String emoji) async {
25+
final emojiId = AssetManager().emojiData?.getIdByEmoji(emoji);
26+
log('$runtimeType::storeRecentReactions: EmojiId is $emojiId');
27+
if (emojiId?.trim().isNotEmpty == true) {
28+
final interactor = getBinding<StoreRecentReactionsInteractor>(
29+
tag: composerId,
30+
);
31+
if (interactor != null) {
32+
consumeState(interactor.execute(emojiId: emojiId!));
33+
}
34+
}
35+
}
36+
37+
Future<Category?> getRecentReactions() async {
38+
final interactor = getBinding<GetRecentReactionsInteractor>(
39+
tag: composerId,
40+
);
41+
if (interactor == null) return Future.value(null);
42+
43+
final result = await interactor.execute();
44+
45+
final category = result.fold(
46+
(failure) => null,
47+
(success) =>
48+
success is GetRecentReactionsSuccess ? success.category : null,
49+
);
50+
log('$runtimeType::getRecentReactions: Category is $category');
51+
return category;
52+
}
1653
}

lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:core/presentation/extensions/color_extension.dart';
22
import 'package:core/presentation/resources/image_paths.dart';
3+
import 'package:core/presentation/utils/responsive_utils.dart';
34
import 'package:core/presentation/views/button/tmail_button_widget.dart';
45
import 'package:core/utils/platform_info.dart';
56
import 'package:custom_pop_up_menu/custom_pop_up_menu.dart';
@@ -15,6 +16,7 @@ import 'package:tmail_ui_user/main/utils/asset_manager.dart';
1516
class BottomBarComposerWidget extends StatelessWidget {
1617

1718
final ImagePaths imagePaths;
19+
final ResponsiveUtils responsiveUtils;
1820
final bool isCodeViewEnabled;
1921
final bool isEmailChanged;
2022
final bool isFormattingOptionsEnabled;
@@ -36,10 +38,12 @@ class BottomBarComposerWidget extends StatelessWidget {
3638
final OnEmojiSelected onEmojiSelected;
3739
final VoidCallback onPickerOpen;
3840
final OnMenuChanged? onPopupMenuChanged;
41+
final OnRecentEmojiSelected? onRecentEmojiSelected;
3942

4043
const BottomBarComposerWidget({
4144
super.key,
4245
required this.imagePaths,
46+
required this.responsiveUtils,
4347
required this.isCodeViewEnabled,
4448
required this.isEmailChanged,
4549
required this.isFormattingOptionsEnabled,
@@ -61,6 +65,7 @@ class BottomBarComposerWidget extends StatelessWidget {
6165
required this.onEmojiSelected,
6266
required this.onPickerOpen,
6367
this.onPopupMenuChanged,
68+
this.onRecentEmojiSelected,
6469
});
6570

6671
@override
@@ -117,7 +122,9 @@ class BottomBarComposerWidget extends StatelessWidget {
117122
onTapActionCallback: insertImageAction,
118123
),
119124
),
120-
if (PlatformInfo.isWeb && AssetManager().emojiData != null)
125+
if (PlatformInfo.isWeb &&
126+
!responsiveUtils.isMobile(context) &&
127+
AssetManager().emojiData != null)
121128
...[
122129
const SizedBox(width: BottomBarComposerWidgetStyle.space),
123130
EmojiButton(
@@ -130,6 +137,7 @@ class BottomBarComposerWidget extends StatelessWidget {
130137
iconPadding: BottomBarComposerWidgetStyle.iconPadding,
131138
onEmojiSelected: onEmojiSelected,
132139
onPickerOpen: onPickerOpen,
140+
onRecentEmojiSelected: onRecentEmojiSelected,
133141
),
134142
],
135143
const SizedBox(width: BottomBarComposerWidgetStyle.space),
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
abstract class ReactionsDatasource {
2+
Future<void> storeRecentReactions(List<String> recentReactions);
3+
4+
Future<List<String>> getRecentReactions();
5+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import 'package:tmail_ui_user/features/reactions/data/datasource/reactions_datasource.dart';
2+
import 'package:tmail_ui_user/features/reactions/data/local/reaction_cache_manager.dart';
3+
import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart';
4+
5+
class ReactionsDatasourceImpl implements ReactionsDatasource {
6+
final ReactionsCacheManager _reactionCacheManager;
7+
final ExceptionThrower _exceptionThrower;
8+
9+
ReactionsDatasourceImpl(this._reactionCacheManager, this._exceptionThrower);
10+
11+
@override
12+
Future<List<String>> getRecentReactions() async {
13+
return Future.sync(() {
14+
return _reactionCacheManager.getRecentReactions() ?? <String>[];
15+
}).catchError((error, stackTrace) async {
16+
await _exceptionThrower.throwException(error, stackTrace);
17+
throw error;
18+
});
19+
}
20+
21+
@override
22+
Future<void> storeRecentReactions(List<String> recentReactions) {
23+
return Future.sync(() async {
24+
return await _reactionCacheManager.storeRecentReactions(recentReactions);
25+
}).catchError((error, stackTrace) async {
26+
await _exceptionThrower.throwException(error, stackTrace);
27+
throw error;
28+
});
29+
}
30+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import 'package:shared_preferences/shared_preferences.dart';
2+
3+
class ReactionsCacheManager {
4+
static const keyRecentReactions = 'RECENT_REACTIONS';
5+
6+
final SharedPreferences _sharedPreferences;
7+
8+
ReactionsCacheManager(this._sharedPreferences);
9+
10+
Future<void> storeRecentReactions(List<String> recentReactions) async {
11+
await _sharedPreferences.setStringList(keyRecentReactions, recentReactions);
12+
}
13+
14+
List<String>? getRecentReactions() {
15+
return _sharedPreferences.getStringList(keyRecentReactions);
16+
}
17+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import 'package:tmail_ui_user/features/reactions/data/datasource/reactions_datasource.dart';
2+
import 'package:tmail_ui_user/features/reactions/domain/repository/reactions_repository.dart';
3+
4+
class ReactionsRepositoryImpl implements ReactionsRepository {
5+
final ReactionsDatasource _dataSource;
6+
7+
ReactionsRepositoryImpl(this._dataSource);
8+
9+
@override
10+
Future<List<String>> getRecentReactions() {
11+
return _dataSource.getRecentReactions();
12+
}
13+
14+
@override
15+
Future<void> storeRecentReactions(List<String> recentReactions) {
16+
return _dataSource.storeRecentReactions(recentReactions);
17+
}
18+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
extension ListReactionsExtension on List<String> {
2+
static const int maxRecentReactionsSize = 12;
3+
4+
List<String> combineRecentReactions(String emojiId) {
5+
final result = List<String>.from(this);
6+
7+
result.remove(emojiId);
8+
result.insert(0, emojiId);
9+
10+
if (result.length > maxRecentReactionsSize) {
11+
result.length = maxRecentReactionsSize;
12+
}
13+
14+
return result;
15+
}
16+
}

0 commit comments

Comments
 (0)