import 'package:appflowy/date/date_service.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:nanoid/nanoid.dart'; final _keywords = [ LocaleKeys.inlineActions_reminder_groupTitle.tr().toLowerCase(), LocaleKeys.inlineActions_reminder_shortKeyword.tr().toLowerCase(), ]; class ReminderReferenceService { ReminderReferenceService(this.context) { // Initialize locale _locale = context.locale.toLanguageTag(); // Initializes options _setOptions(); } final BuildContext context; late String _locale; late List _allOptions; List options = []; Future reminderReferenceDelegate([ String? search, ]) async { // Checks if Locale has changed since last _setLocale(); // Filters static options _filterOptions(search); // Searches for date by pattern _searchDate(search); // Searches for date by natural language prompt await _searchDateNLP(search); return _groupFromResults(options); } InlineActionsResult _groupFromResults([ List? options, ]) => InlineActionsResult( title: LocaleKeys.inlineActions_reminder_groupTitle.tr(), results: options ?? [], startsWithKeywords: [ LocaleKeys.inlineActions_reminder_groupTitle.tr().toLowerCase(), LocaleKeys.inlineActions_reminder_shortKeyword.tr().toLowerCase(), ], ); void _filterOptions(String? search) { if (search == null || search.isEmpty) { options = _allOptions; return; } options = _allOptions .where( (option) => option.keywords != null && option.keywords!.isNotEmpty && option.keywords!.any( (keyword) => keyword.contains(search.toLowerCase()), ), ) .toList(); if (options.isEmpty && _keywords.any((k) => search.startsWith(k))) { options = _allOptions; } } void _searchDate(String? search) { if (search == null || search.isEmpty) { return; } try { final date = DateFormat.yMd(_locale).parse(search); options.insert(0, _itemFromDate(date)); } catch (_) { return; } } Future _searchDateNLP(String? search) async { if (search == null || search.isEmpty) { return; } final result = await DateService.queryDate(search); result.fold( (l) {}, (date) { // Only insert dates in the future if (DateTime.now().isBefore(date)) { options.insert(0, _itemFromDate(date)); } }, ); } Future _insertReminderReference( EditorState editorState, DateTime date, int start, int end, ) async { final selection = editorState.selection; if (selection == null || !selection.isCollapsed) { return; } final node = editorState.getNodeAtPath(selection.end.path); final delta = node?.delta; if (node == null || delta == null) { return; } final viewId = context.read().view.id; final reminder = _reminderFromDate(date, viewId); context.read().add(ReminderEvent.add(reminder: reminder)); final transaction = editorState.transaction ..replaceText( node, start, end, '\$', attributes: { MentionBlockKeys.mention: { MentionBlockKeys.type: MentionType.reminder.name, MentionBlockKeys.date: date.toIso8601String(), MentionBlockKeys.uid: reminder.id, } }, ); await editorState.apply(transaction); } void _setOptions() { final today = DateTime.now(); final tomorrow = today.add(const Duration(days: 1)); final oneWeek = today.add(const Duration(days: 7)); _allOptions = [ _itemFromDate( tomorrow, LocaleKeys.relativeDates_tomorrow.tr(), [DateFormat.yMd(_locale).format(tomorrow)], ), _itemFromDate( oneWeek, LocaleKeys.relativeDates_oneWeek.tr(), [DateFormat.yMd(_locale).format(oneWeek)], ), ]; } /// Sets Locale on each search to make sure /// keywords are localized void _setLocale() { final locale = context.locale.toLanguageTag(); if (locale != _locale) { _locale = locale; _setOptions(); } } InlineActionsMenuItem _itemFromDate( DateTime date, [ String? label, List? keywords, ]) { final labelStr = label ?? DateFormat.yMd(_locale).format(date); return InlineActionsMenuItem( label: labelStr.capitalize(), keywords: [labelStr.toLowerCase(), ...?keywords], onSelected: (context, editorState, menuService, replace) => _insertReminderReference(editorState, date, replace.$1, replace.$2), ); } ReminderPB _reminderFromDate(DateTime date, String viewId) { return ReminderPB( id: nanoid(), objectId: viewId, title: LocaleKeys.reminderNotification_title.tr(), message: LocaleKeys.reminderNotification_message.tr(), meta: {"document_id": viewId}, scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000), isAck: date.isBefore(DateTime.now()), ); } }