From e1474c48d8d45672400a72b1de5ff17405c854da Mon Sep 17 00:00:00 2001 From: krille-chan Date: Fri, 22 Dec 2023 20:20:58 +0100 Subject: [PATCH] refactor: Remove todo list feature --- lib/config/routes.dart | 12 - lib/pages/tasks/model/matrix_todo_list.dart | 59 ----- lib/pages/tasks/tasks.dart | 255 ------------------- lib/pages/tasks/tasks_view.dart | 234 ----------------- lib/utils/localized_exception_extension.dart | 4 - lib/widgets/chat_settings_popup_menu.dart | 10 - 6 files changed, 574 deletions(-) delete mode 100644 lib/pages/tasks/model/matrix_todo_list.dart delete mode 100644 lib/pages/tasks/tasks.dart delete mode 100644 lib/pages/tasks/tasks_view.dart diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 5aaf5cb1..0f6e6dd1 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -28,7 +28,6 @@ import 'package:fluffychat/pages/settings_multiple_emotes/settings_multiple_emot import 'package:fluffychat/pages/settings_notifications/settings_notifications.dart'; import 'package:fluffychat/pages/settings_security/settings_security.dart'; import 'package:fluffychat/pages/settings_style/settings_style.dart'; -import 'package:fluffychat/pages/tasks/tasks.dart'; import 'package:fluffychat/widgets/layouts/empty_page.dart'; import 'package:fluffychat/widgets/layouts/two_column_layout.dart'; import 'package:fluffychat/widgets/log_view.dart'; @@ -359,17 +358,6 @@ abstract class AppRoutes { ], redirect: loggedOutRedirect, ), - GoRoute( - path: 'tasks', - pageBuilder: (context, state) => defaultPageBuilder( - context, - TasksPage( - room: Matrix.of(context) - .client - .getRoomById(state.pathParameters['roomid']!)!, - ), - ), - ), ], ), ], diff --git a/lib/pages/tasks/model/matrix_todo_list.dart b/lib/pages/tasks/model/matrix_todo_list.dart deleted file mode 100644 index fdcb8897..00000000 --- a/lib/pages/tasks/model/matrix_todo_list.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:matrix/matrix.dart'; - -extension MatrixTodoExtension on Room { - static const String stateKey = 'im.fluffychat.matrix_todos'; - static const String contentKey = 'todos'; - - List? get matrixTodos => getState(stateKey) - ?.content - .tryGetList(contentKey) - ?.map((json) => MatrixTodo.fromJson(json)) - .toList(); - - Future updateMatrixTodos(List matrixTodos) => - client.setRoomStateWithKey( - id, - stateKey, - '', - {contentKey: matrixTodos.map((todo) => todo.toJson()).toList()}, - ); -} - -class MatrixTodo { - String title; - String? description; - DateTime? dueDate; - bool done; - List? subTasks; - - MatrixTodo({ - required this.title, - this.description, - this.dueDate, - this.done = false, - this.subTasks, - }); - - factory MatrixTodo.fromJson(Map json) => MatrixTodo( - title: json['title'] as String, - description: json['description'] as String?, - dueDate: json['due_date'] == null - ? null - : DateTime.fromMillisecondsSinceEpoch(json['due_date'] as int), - done: json['done'] as bool, - subTasks: json['sub_tasks'] == null - ? null - : (json['sub_tasks'] as List) - .map((json) => MatrixTodo.fromJson(json)) - .toList(), - ); - - Map toJson() => { - 'title': title, - if (description != null) 'description': description, - if (dueDate != null) 'due_date': dueDate?.millisecondsSinceEpoch, - 'done': done, - if (subTasks != null) - 'sub_tasks': subTasks?.map((t) => t.toJson()).toList(), - }; -} diff --git a/lib/pages/tasks/tasks.dart b/lib/pages/tasks/tasks.dart deleted file mode 100644 index 7743b99e..00000000 --- a/lib/pages/tasks/tasks.dart +++ /dev/null @@ -1,255 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/pages/tasks/tasks_view.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; -import 'model/matrix_todo_list.dart'; - -class TasksPage extends StatefulWidget { - final Room room; - const TasksPage({required this.room, super.key}); - - @override - State createState() => TasksController(); -} - -class TasksController extends State { - bool isLoading = false; - DateTime? newTaskDateTime; - String? newTaskDescription; - - final FocusNode focusNode = FocusNode(); - final TextEditingController textEditingController = TextEditingController(); - - List? _tmpTodos; - - List get todos => _tmpTodos ?? widget.room.matrixTodos ?? []; - - Stream get onUpdate => widget.room.client.onSync.stream.where( - (syncUpdate) => - syncUpdate.rooms?.join?[widget.room.id]?.state - ?.any((event) => event.type == MatrixTodoExtension.stateKey) ?? - false, - ); - - void setNewTaskDateTime() async { - final now = DateTime.now(); - final date = await showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: now.subtract(const Duration(days: 365 * 100)), - lastDate: now.add(const Duration(days: 365 * 100)), - ); - if (date == null) return; - setState(() { - newTaskDateTime = date; - }); - } - - void setNewTaskDescription() async { - final text = await showTextInputDialog( - context: context, - title: L10n.of(context)!.addDescription, - textFields: [ - DialogTextField( - hintText: L10n.of(context)!.addDescription, - maxLength: 512, - minLines: 4, - maxLines: 8, - ), - ], - ); - if (text == null || text.single.isEmpty) return; - setState(() { - newTaskDescription = text.single; - }); - } - - void addTodo([_]) { - if (textEditingController.text.isEmpty) return; - updateTodos( - update: (todos) => [ - ...todos, - MatrixTodo( - title: textEditingController.text, - dueDate: newTaskDateTime, - description: newTaskDescription, - ), - ], - onSuccess: () { - newTaskDateTime = null; - newTaskDescription = null; - textEditingController.clear(); - focusNode.requestFocus(); - }, - ); - } - - void toggleDone(int i) => updateTodos( - update: (todos) { - todos[i].done = !todos[i].done; - return todos; - }, - ); - - void cleanUp() => updateTodos( - update: (todos) => todos..removeWhere((t) => t.done), - ); - - void onReorder(int oldindex, int newindex) { - if (newindex > oldindex) { - newindex -= 1; - } - updateTodos( - update: (todos) { - final todo = todos.removeAt(oldindex); - todos.insert(newindex, todo); - return todos; - }, - tmpTodo: true, - ); - } - - void updateTodos({ - required List Function(List) update, - void Function()? onSuccess, - bool tmpTodo = false, - }) async { - setState(() { - isLoading = true; - }); - try { - final newTodos = update(todos); - assert(todos != newTodos); - if (tmpTodo) { - setState(() { - _tmpTodos = newTodos; - }); - onUpdate.first.then((_) { - _tmpTodos = null; - }); - } - await widget.room - .updateMatrixTodos(newTodos) - .timeout(const Duration(seconds: 30)); - onSuccess?.call(); - } on MatrixException catch (e) { - final retryAfterMs = e.retryAfterMs; - if (retryAfterMs == null) rethrow; - Logs().w('Rate limit! Try again in $retryAfterMs ms'); - await Future.delayed(Duration(milliseconds: retryAfterMs)); - updateTodos(update: update, onSuccess: onSuccess); - } catch (e, s) { - Logs().w('Unable to update todo list', e, s); - if (_tmpTodos != null) { - setState(() { - _tmpTodos = null; - }); - } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - duration: const Duration(seconds: 20), - content: Row( - children: [ - Icon( - Icons.signal_wifi_connected_no_internet_4_outlined, - color: Theme.of(context).colorScheme.background, - ), - const SizedBox(width: 16), - Expanded( - child: Text( - e.toLocalizedString(context), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - action: e is TodoListChangedException - ? null - : SnackBarAction( - label: 'Try again', - onPressed: () { - updateTodos(update: update, onSuccess: onSuccess); - }, - ), - ), - ); - } finally { - setState(() { - isLoading = false; - }); - } - } - - void editTodo(int i, MatrixTodo todo) async { - final texts = await showTextInputDialog( - context: context, - title: L10n.of(context)!.editTodo, - textFields: [ - DialogTextField( - hintText: L10n.of(context)!.newTodo, - initialText: todo.title, - maxLength: 64, - validator: (text) { - if (text == null) return L10n.of(context)!.pleaseAddATitle; - return null; - }, - ), - DialogTextField( - hintText: L10n.of(context)!.addDescription, - maxLength: 512, - minLines: 4, - maxLines: 8, - initialText: todo.description, - ), - ], - ); - if (texts == null) return; - updateTodos( - update: (todos) { - if (todos[i].toJson().toString() != todo.toJson().toString()) { - throw TodoListChangedException(); - } - todos[i].title = texts[0]; - todos[i].description = texts[1].isEmpty ? null : texts[1]; - return todos; - }, - ); - } - - void deleteTodo(int i) => updateTodos( - update: (list) { - list.removeAt(i); - return list; - }, - ); - - void editTodoDueDate(int i, MatrixTodo todo) async { - final now = DateTime.now(); - final date = await showDatePicker( - context: context, - initialDate: todo.dueDate ?? DateTime.now(), - firstDate: now.subtract(const Duration(days: 365 * 100)), - lastDate: now.add(const Duration(days: 365 * 100)), - ); - if (date == null) return; - updateTodos( - update: (todos) { - if (todos[i].toJson().toString() != todo.toJson().toString()) { - throw TodoListChangedException(); - } - todos[i].dueDate = date; - return todos; - }, - ); - } - - @override - Widget build(BuildContext context) => TasksView(this); -} - -class TodoListChangedException implements Exception {} diff --git a/lib/pages/tasks/tasks_view.dart b/lib/pages/tasks/tasks_view.dart deleted file mode 100644 index 8046b44d..00000000 --- a/lib/pages/tasks/tasks_view.dart +++ /dev/null @@ -1,234 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:intl/intl.dart'; - -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/pages/tasks/tasks.dart'; - -class TasksView extends StatelessWidget { - final TasksController controller; - const TasksView(this.controller, {super.key}); - - @override - Widget build(BuildContext context) { - final tag = Localizations.maybeLocaleOf(context)?.toLanguageTag(); - return StreamBuilder( - stream: controller.widget.room.onUpdate.stream, - builder: (context, snapshot) { - final list = controller.todos; - return Scaffold( - appBar: AppBar( - title: Text(L10n.of(context)!.todoLists), - actions: [ - AnimatedCrossFade( - duration: FluffyThemes.animationDuration, - firstChild: const SizedBox( - width: 32, - height: 32, - ), - secondChild: const Padding( - padding: EdgeInsets.all(8.0), - child: SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ), - ), - crossFadeState: controller.isLoading - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, - ), - if (list.any((todo) => todo.done)) - IconButton( - icon: const Icon(Icons.cleaning_services_outlined), - onPressed: controller.cleanUp, - ), - ], - ), - body: Column( - children: [ - Expanded( - child: Opacity( - opacity: controller.isLoading ? 0.66 : 1, - child: list.isEmpty - ? Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon( - Icons.task_alt, - size: 80, - color: Theme.of(context).colorScheme.secondary, - ), - const SizedBox(height: 16), - SizedBox( - width: 256, - child: Text( - L10n.of(context)!.noTodosYet, - textAlign: TextAlign.center, - ), - ), - const SizedBox(height: 16), - SizedBox( - width: 256, - child: Text( - L10n.of(context)!.todosUnencrypted, - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.orange), - ), - ), - ], - ) - : ReorderableListView.builder( - onReorder: controller.onReorder, - itemCount: list.length, - buildDefaultDragHandles: false, - itemBuilder: (context, i) { - final todo = list[i]; - final description = todo.description; - final dueDate = todo.dueDate; - return ListTile( - key: Key(todo.toJson().toString()), - leading: Icon( - todo.done - ? Icons.check_circle - : Icons.circle_outlined, - ), - title: Text( - todo.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - decoration: todo.done - ? TextDecoration.lineThrough - : null, - ), - ), - subtitle: description == null && dueDate == null - ? null - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - if (description != null) - Text( - description, - maxLines: 2, - ), - if (dueDate != null) - SizedBox( - height: 24, - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( - padding: - const EdgeInsets.symmetric( - horizontal: 6, - ), - ), - icon: const Icon( - Icons.calendar_month, - size: 16, - ), - label: Text( - DateFormat.yMMMd(tag) - .format(dueDate), - style: const TextStyle( - fontSize: 12, - ), - ), - onPressed: () => - controller.editTodoDueDate( - i, - todo, - ), - ), - ), - ], - ), - onTap: controller.isLoading - ? null - : () => controller.toggleDone(i), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon( - Icons.edit_outlined, - size: 16, - ), - onPressed: () => - controller.editTodo(i, todo), - ), - IconButton( - icon: const Icon( - Icons.delete_outlined, - size: 16, - ), - onPressed: () => controller.deleteTodo(i), - ), - ReorderableDragStartListener( - index: i, - child: - const Icon(Icons.drag_handle_outlined), - ), - ], - ), - ); - }, - ), - ), - ), - Padding( - padding: const EdgeInsets.all(12.0), - child: TextField( - focusNode: controller.focusNode, - readOnly: controller.isLoading, - controller: controller.textEditingController, - onSubmitted: controller.addTodo, - maxLength: 64, - decoration: InputDecoration( - counterStyle: const TextStyle(height: double.minPositive), - counterText: '', - hintText: L10n.of(context)!.newTodo, - prefixIcon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon( - controller.newTaskDateTime == null - ? Icons.calendar_month_outlined - : Icons.calendar_month, - color: controller.newTaskDateTime == null - ? null - : Theme.of(context).primaryColor, - ), - onPressed: controller.setNewTaskDateTime, - ), - IconButton( - icon: Icon( - Icons.text_fields, - color: controller.newTaskDescription == null - ? null - : Theme.of(context).primaryColor, - ), - onPressed: controller.setNewTaskDescription, - ), - ], - ), - suffixIcon: IconButton( - icon: const Icon(Icons.add_outlined), - onPressed: - controller.isLoading ? null : controller.addTodo, - ), - ), - ), - ), - ], - ), - ); - }, - ); - } -} diff --git a/lib/utils/localized_exception_extension.dart b/lib/utils/localized_exception_extension.dart index 35496f2e..abcdcea1 100644 --- a/lib/utils/localized_exception_extension.dart +++ b/lib/utils/localized_exception_extension.dart @@ -6,7 +6,6 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/pages/tasks/tasks.dart'; import 'uia_request_manager.dart'; extension LocalizedExceptionExtension on Object { @@ -24,9 +23,6 @@ extension LocalizedExceptionExtension on Object { if (this is InvalidPassphraseException) { return L10n.of(context)!.wrongRecoveryKey; } - if (this is TodoListChangedException) { - return L10n.of(context)!.todoListChangedError; - } if (this is FileTooBigMatrixException) { return L10n.of(context)!.fileIsTooBigForServer; } diff --git a/lib/widgets/chat_settings_popup_menu.dart b/lib/widgets/chat_settings_popup_menu.dart index 0ccc55b6..277e7170 100644 --- a/lib/widgets/chat_settings_popup_menu.dart +++ b/lib/widgets/chat_settings_popup_menu.dart @@ -63,16 +63,6 @@ class ChatSettingsPopupMenuState extends State { ], ), ), - PopupMenuItem( - value: 'todos', - child: Row( - children: [ - const Icon(Icons.task_alt_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.todoLists), - ], - ), - ), PopupMenuItem( value: 'leave', child: Row(