diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index d5ffaae9d..e7420faa7 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -696,6 +696,7 @@ } } }, + "checkList": "Check list", "countParticipants": "{count} participants", "@countParticipants": { "type": "String", diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index c1e5ee84b..db309551b 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -336,9 +336,10 @@ class ChatController extends State ); } - KeyEventResult _shiftEnterKeyHandling(FocusNode node, KeyEvent evt) { + KeyEventResult _customEnterKeyHandling(FocusNode node, KeyEvent evt) { if (!HardwareKeyboard.instance.isShiftPressed && - evt.logicalKey.keyLabel == 'Enter') { + evt.logicalKey.keyLabel == 'Enter' && + (AppConfig.sendOnEnter ?? !PlatformInfos.isMobile)) { if (evt is KeyDownEvent) { // #Pangea // send(); @@ -346,6 +347,36 @@ class ChatController extends State // Pangea# } return KeyEventResult.handled; + } else if (evt.logicalKey.keyLabel == 'Enter' && evt is KeyDownEvent) { + final currentLineNum = sendController.text + .substring( + 0, + sendController.selection.baseOffset, + ) + .split('\n') + .length - + 1; + final currentLine = sendController.text.split('\n')[currentLineNum]; + + for (final pattern in [ + '- [ ] ', + '- [x] ', + '* [ ] ', + '* [x] ', + '- ', + '* ', + '+ ', + ]) { + if (currentLine.startsWith(pattern)) { + if (currentLine == pattern) { + return KeyEventResult.ignored; + } + sendController.text += '\n$pattern'; + return KeyEventResult.handled; + } + } + + return KeyEventResult.ignored; } else { return KeyEventResult.ignored; } @@ -353,11 +384,7 @@ class ChatController extends State @override void initState() { - inputFocus = FocusNode( - onKeyEvent: (AppConfig.sendOnEnter ?? !PlatformInfos.isMobile) - ? _shiftEnterKeyHandling - : null, - ); + inputFocus = FocusNode(onKeyEvent: _customEnterKeyHandling); scrollController.addListener(_updateScrollController); inputFocus.addListener(_inputFocusListener); @@ -1748,6 +1775,14 @@ class ChatController extends State if (choice == 'location') { sendLocationAction(); } + if (choice == 'checklist') { + if (sendController.text.isEmpty) { + sendController.text = '- [ ] '; + } else { + sendController.text += '\n- [ ] '; + } + inputFocus.requestFocus(); + } } unpinEvent(String eventId) async { diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index c500f251f..e0ed099db 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -123,6 +123,18 @@ class ChatInputRow extends StatelessWidget { onSelected: controller.onAddPopupMenuButtonSelected, itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: 'checklist', + child: ListTile( + leading: CircleAvatar( + backgroundColor: theme.colorScheme.onPrimaryContainer, + foregroundColor: theme.colorScheme.primaryContainer, + child: const Icon(Icons.check_circle_outlined), + ), + title: Text(L10n.of(context).checkList), + contentPadding: const EdgeInsets.all(0), + ), + ), if (PlatformInfos.isMobile) PopupMenuItem( value: 'location', diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index f83b04689..d4e78cc19 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -18,7 +18,9 @@ import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/utils/token_rendering_util.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; +import 'package:fluffychat/utils/event_checkbox_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import '../../../utils/url_launcher.dart'; @@ -30,6 +32,9 @@ class HtmlMessage extends StatelessWidget { final double fontSize; final TextStyle linkStyle; final void Function(LinkableElement) onOpen; + final String? eventId; + final Set? checkboxCheckedEvents; + // #Pangea final MessageOverlayController? overlayController; final PangeaMessageEvent? pangeaMessageEvent; @@ -52,6 +57,8 @@ class HtmlMessage extends StatelessWidget { required this.linkStyle, this.textColor = Colors.black, required this.onOpen, + this.eventId, + this.checkboxCheckedEvents, // #Pangea this.overlayController, required this.event, @@ -460,6 +467,24 @@ class HtmlMessage extends StatelessWidget { if (!{'ol', 'ul'}.contains(node.parent?.localName)) { continue block; } + final eventId = this.eventId; + + final isCheckbox = node.className == 'task-list-item'; + final checkboxIndex = isCheckbox + ? node.rootElement + .getElementsByClassName('task-list-item') + .indexOf(node) + + 1 + : null; + final checkedByReaction = !isCheckbox + ? null + : checkboxCheckedEvents?.firstWhereOrNull( + (event) => event.checkedCheckboxId == checkboxIndex, + ); + final staticallyChecked = !isCheckbox + ? false + : node.children.first.attributes['checked'] == 'true'; + return WidgetSpan( child: Padding( padding: EdgeInsets.only(left: fontSize), @@ -476,6 +501,42 @@ class HtmlMessage extends StatelessWidget { text: '${(node.parent?.nodes.whereType().toList().indexOf(node) ?? 0) + (int.tryParse(node.parent?.attributes['start'] ?? '1') ?? 1)}. ', ), + if (node.className == 'task-list-item') + WidgetSpan( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: SizedBox.square( + dimension: fontSize, + child: Checkbox.adaptive( + checkColor: textColor, + side: BorderSide(color: textColor), + activeColor: textColor.withAlpha(64), + visualDensity: VisualDensity.compact, + value: + staticallyChecked || checkedByReaction != null, + onChanged: eventId == null || + checkboxIndex == null || + staticallyChecked || + !room.canSendDefaultMessages || + (checkedByReaction != null && + checkedByReaction.senderId != + room.client.userID) + ? null + : (_) => showFutureLoadingDialog( + context: context, + future: () => checkedByReaction != null + ? room.redactEvent( + checkedByReaction.eventId, + ) + : room.checkCheckbox( + eventId, + checkboxIndex, + ), + ), + ), + ), + ), + ), ..._renderWithLineBreaks( node.nodes, context, @@ -761,7 +822,14 @@ class HtmlMessage extends StatelessWidget { @override Widget build(BuildContext context) { // #Pangea + // final element = parser.parse(html).body ?? dom.Element.html(''); // return Text.rich( + // _renderHtml(element, context), + // style: TextStyle( + // fontSize: fontSize, + // color: textColor, + // ), + // ); dom.Node parsed = parser.parse(html).body ?? dom.Element.html(''); if (tokens != null) { parsed = _tokenizeHtml(parsed, html, List.from(tokens!)); @@ -780,19 +848,13 @@ class HtmlMessage extends StatelessWidget { }, child: Text.rich( textScaler: TextScaler.noScaling, - // Pangea# _renderHtml( - // #Pangea - // parser.parse(html).body ?? dom.Element.html(''), parsed, - // Pangea# context, - // #Pangea TextStyle( fontSize: fontSize, color: textColor, ), - // Pangea# ), style: TextStyle( fontSize: fontSize, @@ -870,3 +932,7 @@ extension on String { return colorValue == null ? null : Color(colorValue); } } + +extension on dom.Element { + dom.Element get rootElement => parent?.rootElement ?? this; +} diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 0e1892ad1..a711070e0 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -19,6 +19,7 @@ import 'package:fluffychat/pangea/toolbar/widgets/message_token_text.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_toolbar_selection_area.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import '../../../config/app_config.dart'; +import '../../../utils/event_checkbox_extension.dart'; import '../../../utils/platform_infos.dart'; import '../../../utils/url_launcher.dart'; import 'audio_player.dart'; @@ -291,6 +292,11 @@ class MessageContent extends StatelessWidget { decorationColor: linkColor, ), onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), + eventId: event.eventId, + checkboxCheckedEvents: event.aggregatedEvents( + timeline, + EventCheckboxRoomExtension.relationshipType, + ), // #Pangea event: event, overlayController: overlayController, diff --git a/lib/pangea/chat/widgets/pangea_chat_input_row.dart b/lib/pangea/chat/widgets/pangea_chat_input_row.dart index cb6df5877..a7bfa30d3 100644 --- a/lib/pangea/chat/widgets/pangea_chat_input_row.dart +++ b/lib/pangea/chat/widgets/pangea_chat_input_row.dart @@ -218,6 +218,21 @@ class PangeaChatInputRowState extends State { onSelected: _controller.onAddPopupMenuButtonSelected, itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: 'checklist', + child: ListTile( + leading: CircleAvatar( + backgroundColor: + theme.colorScheme.onPrimaryContainer, + foregroundColor: + theme.colorScheme.primaryContainer, + child: + const Icon(Icons.check_circle_outlined), + ), + title: Text(L10n.of(context).checkList), + contentPadding: const EdgeInsets.all(0), + ), + ), PopupMenuItem( value: 'file', child: ListTile( diff --git a/lib/utils/event_checkbox_extension.dart b/lib/utils/event_checkbox_extension.dart new file mode 100644 index 000000000..cf3832ba6 --- /dev/null +++ b/lib/utils/event_checkbox_extension.dart @@ -0,0 +1,27 @@ +import 'package:matrix/matrix.dart'; + +extension EventCheckboxRoomExtension on Room { + static const String relationshipType = 'im.fluffychat.checkboxes'; + Future checkCheckbox( + String eventId, + int checkboxId, { + String? txid, + }) => + sendEvent( + { + 'm.relates_to': { + 'rel_type': relationshipType, + 'event_id': eventId, + 'checkbox_id': checkboxId, + }, + }, + type: EventTypes.Reaction, + txid: txid, + ); +} + +extension EventCheckboxExtension on Event { + int? get checkedCheckboxId => content + .tryGetMap('m.relates_to') + ?.tryGet('checkbox_id'); +} diff --git a/pubspec.yaml b/pubspec.yaml index 3b1be922e..ab0f1992e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -207,4 +207,4 @@ dependency_overrides: url: https://github.com/ThexXTURBOXx/flutter_web_auth_2.git ref: 3.x-without-v1 path: flutter_web_auth_2 - win32: 5.5.3 + win32: 5.5.3 \ No newline at end of file