reminder_reference.dart 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import 'package:appflowy/date/date_service.dart';
  2. import 'package:appflowy/generated/locale_keys.g.dart';
  3. import 'package:appflowy/plugins/document/application/doc_bloc.dart';
  4. import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
  5. import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
  6. import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
  7. import 'package:appflowy/user/application/reminder/reminder_extension.dart';
  8. import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
  9. import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';
  10. import 'package:appflowy_editor/appflowy_editor.dart';
  11. import 'package:easy_localization/easy_localization.dart';
  12. import 'package:fixnum/fixnum.dart';
  13. import 'package:flutter/material.dart';
  14. import 'package:flutter_bloc/flutter_bloc.dart';
  15. import 'package:nanoid/nanoid.dart';
  16. final _keywords = [
  17. LocaleKeys.inlineActions_reminder_groupTitle.tr().toLowerCase(),
  18. LocaleKeys.inlineActions_reminder_shortKeyword.tr().toLowerCase(),
  19. ];
  20. class ReminderReferenceService {
  21. ReminderReferenceService(this.context) {
  22. // Initialize locale
  23. _locale = context.locale.toLanguageTag();
  24. // Initializes options
  25. _setOptions();
  26. }
  27. final BuildContext context;
  28. late String _locale;
  29. late List<InlineActionsMenuItem> _allOptions;
  30. List<InlineActionsMenuItem> options = [];
  31. Future<InlineActionsResult> reminderReferenceDelegate([
  32. String? search,
  33. ]) async {
  34. // Checks if Locale has changed since last
  35. _setLocale();
  36. // Filters static options
  37. _filterOptions(search);
  38. // Searches for date by pattern
  39. _searchDate(search);
  40. // Searches for date by natural language prompt
  41. await _searchDateNLP(search);
  42. return _groupFromResults(options);
  43. }
  44. InlineActionsResult _groupFromResults([
  45. List<InlineActionsMenuItem>? options,
  46. ]) =>
  47. InlineActionsResult(
  48. title: LocaleKeys.inlineActions_reminder_groupTitle.tr(),
  49. results: options ?? [],
  50. startsWithKeywords: [
  51. LocaleKeys.inlineActions_reminder_groupTitle.tr().toLowerCase(),
  52. LocaleKeys.inlineActions_reminder_shortKeyword.tr().toLowerCase(),
  53. ],
  54. );
  55. void _filterOptions(String? search) {
  56. if (search == null || search.isEmpty) {
  57. options = _allOptions;
  58. return;
  59. }
  60. options = _allOptions
  61. .where(
  62. (option) =>
  63. option.keywords != null &&
  64. option.keywords!.isNotEmpty &&
  65. option.keywords!.any(
  66. (keyword) => keyword.contains(search.toLowerCase()),
  67. ),
  68. )
  69. .toList();
  70. if (options.isEmpty && _keywords.any((k) => search.startsWith(k))) {
  71. _setOptions();
  72. options = _allOptions;
  73. }
  74. }
  75. void _searchDate(String? search) {
  76. if (search == null || search.isEmpty) {
  77. return;
  78. }
  79. try {
  80. final date = DateFormat.yMd(_locale).parse(search);
  81. options.insert(0, _itemFromDate(date));
  82. } catch (_) {
  83. return;
  84. }
  85. }
  86. Future<void> _searchDateNLP(String? search) async {
  87. if (search == null || search.isEmpty) {
  88. return;
  89. }
  90. final result = await DateService.queryDate(search);
  91. result.fold(
  92. (l) {},
  93. (date) {
  94. // Only insert dates in the future
  95. if (DateTime.now().isBefore(date)) {
  96. options.insert(0, _itemFromDate(date));
  97. }
  98. },
  99. );
  100. }
  101. Future<void> _insertReminderReference(
  102. EditorState editorState,
  103. DateTime date,
  104. int start,
  105. int end,
  106. ) async {
  107. final selection = editorState.selection;
  108. if (selection == null || !selection.isCollapsed) {
  109. return;
  110. }
  111. final node = editorState.getNodeAtPath(selection.end.path);
  112. final delta = node?.delta;
  113. if (node == null || delta == null) {
  114. return;
  115. }
  116. final viewId = context.read<DocumentBloc>().view.id;
  117. final reminder = _reminderFromDate(date, viewId);
  118. context.read<ReminderBloc>().add(ReminderEvent.add(reminder: reminder));
  119. final transaction = editorState.transaction
  120. ..replaceText(
  121. node,
  122. start,
  123. end,
  124. '\$',
  125. attributes: {
  126. MentionBlockKeys.mention: {
  127. MentionBlockKeys.type: MentionType.reminder.name,
  128. MentionBlockKeys.date: date.toIso8601String(),
  129. MentionBlockKeys.uid: reminder.id,
  130. }
  131. },
  132. );
  133. await editorState.apply(transaction);
  134. }
  135. void _setOptions() {
  136. final today = DateTime.now();
  137. final tomorrow = today.add(const Duration(days: 1));
  138. final oneWeek = today.add(const Duration(days: 7));
  139. _allOptions = [
  140. _itemFromDate(
  141. tomorrow,
  142. LocaleKeys.relativeDates_tomorrow.tr(),
  143. [DateFormat.yMd(_locale).format(tomorrow)],
  144. ),
  145. _itemFromDate(
  146. oneWeek,
  147. LocaleKeys.relativeDates_oneWeek.tr(),
  148. [DateFormat.yMd(_locale).format(oneWeek)],
  149. ),
  150. ];
  151. }
  152. /// Sets Locale on each search to make sure
  153. /// keywords are localized
  154. void _setLocale() {
  155. final locale = context.locale.toLanguageTag();
  156. if (locale != _locale) {
  157. _locale = locale;
  158. _setOptions();
  159. }
  160. }
  161. InlineActionsMenuItem _itemFromDate(
  162. DateTime date, [
  163. String? label,
  164. List<String>? keywords,
  165. ]) {
  166. final labelStr = label ?? DateFormat.yMd(_locale).format(date);
  167. return InlineActionsMenuItem(
  168. label: labelStr.capitalize(),
  169. keywords: [labelStr.toLowerCase(), ...?keywords],
  170. onSelected: (context, editorState, menuService, replace) =>
  171. _insertReminderReference(editorState, date, replace.$1, replace.$2),
  172. );
  173. }
  174. ReminderPB _reminderFromDate(DateTime date, String viewId) {
  175. return ReminderPB(
  176. id: nanoid(),
  177. objectId: viewId,
  178. title: LocaleKeys.reminderNotification_title.tr(),
  179. message: LocaleKeys.reminderNotification_message.tr(),
  180. meta: {
  181. ReminderMetaKeys.includeTime.name: false.toString(),
  182. },
  183. scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000),
  184. isAck: date.isBefore(DateTime.now()),
  185. );
  186. }
  187. }