reminder_reference.dart 5.9 KB

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