reminder_reference.dart 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  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. options = _allOptions;
  71. }
  72. }
  73. void _searchDate(String? search) {
  74. if (search == null || search.isEmpty) {
  75. return;
  76. }
  77. try {
  78. final date = DateFormat.yMd(_locale).parse(search);
  79. options.insert(0, _itemFromDate(date));
  80. } catch (_) {
  81. return;
  82. }
  83. }
  84. Future<void> _searchDateNLP(String? search) async {
  85. if (search == null || search.isEmpty) {
  86. return;
  87. }
  88. final result = await DateService.queryDate(search);
  89. result.fold(
  90. (l) {},
  91. (date) {
  92. // Only insert dates in the future
  93. if (DateTime.now().isBefore(date)) {
  94. options.insert(0, _itemFromDate(date));
  95. }
  96. },
  97. );
  98. }
  99. Future<void> _insertReminderReference(
  100. EditorState editorState,
  101. DateTime date,
  102. int start,
  103. int end,
  104. ) async {
  105. final selection = editorState.selection;
  106. if (selection == null || !selection.isCollapsed) {
  107. return;
  108. }
  109. final node = editorState.getNodeAtPath(selection.end.path);
  110. final delta = node?.delta;
  111. if (node == null || delta == null) {
  112. return;
  113. }
  114. final viewId = context.read<DocumentBloc>().view.id;
  115. final reminder = _reminderFromDate(date, viewId);
  116. context.read<ReminderBloc>().add(ReminderEvent.add(reminder: reminder));
  117. final transaction = editorState.transaction
  118. ..replaceText(
  119. node,
  120. start,
  121. end,
  122. '\$',
  123. attributes: {
  124. MentionBlockKeys.mention: {
  125. MentionBlockKeys.type: MentionType.reminder.name,
  126. MentionBlockKeys.date: date.toIso8601String(),
  127. MentionBlockKeys.uid: reminder.id,
  128. }
  129. },
  130. );
  131. await editorState.apply(transaction);
  132. }
  133. void _setOptions() {
  134. final today = DateTime.now();
  135. final tomorrow = today.add(const Duration(days: 1));
  136. final oneWeek = today.add(const Duration(days: 7));
  137. _allOptions = [
  138. _itemFromDate(
  139. tomorrow,
  140. LocaleKeys.relativeDates_tomorrow.tr(),
  141. [DateFormat.yMd(_locale).format(tomorrow)],
  142. ),
  143. _itemFromDate(
  144. oneWeek,
  145. LocaleKeys.relativeDates_oneWeek.tr(),
  146. [DateFormat.yMd(_locale).format(oneWeek)],
  147. ),
  148. ];
  149. }
  150. /// Sets Locale on each search to make sure
  151. /// keywords are localized
  152. void _setLocale() {
  153. final locale = context.locale.toLanguageTag();
  154. if (locale != _locale) {
  155. _locale = locale;
  156. _setOptions();
  157. }
  158. }
  159. InlineActionsMenuItem _itemFromDate(
  160. DateTime date, [
  161. String? label,
  162. List<String>? keywords,
  163. ]) {
  164. final labelStr = label ?? DateFormat.yMd(_locale).format(date);
  165. return InlineActionsMenuItem(
  166. label: labelStr.capitalize(),
  167. keywords: [labelStr.toLowerCase(), ...?keywords],
  168. onSelected: (context, editorState, menuService, replace) =>
  169. _insertReminderReference(editorState, date, replace.$1, replace.$2),
  170. );
  171. }
  172. ReminderPB _reminderFromDate(DateTime date, String viewId) {
  173. return ReminderPB(
  174. id: nanoid(),
  175. objectId: viewId,
  176. title: LocaleKeys.reminderNotification_title.tr(),
  177. message: LocaleKeys.reminderNotification_message.tr(),
  178. meta: {"document_id": viewId},
  179. scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000),
  180. isAck: date.isBefore(DateTime.now()),
  181. );
  182. }
  183. }