feat: Check markdown checkboxes in messages

pull/1859/head
krille-chan 3 months ago
parent 7cc341ac91
commit a2e5a940bd
No known key found for this signature in database

@ -8,7 +8,9 @@ import 'package:html/dom.dart' as dom;
import 'package:html/parser.dart' as parser; import 'package:html/parser.dart' as parser;
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/event_checkbox_extension.dart';
import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:fluffychat/widgets/mxc_image.dart';
import '../../../utils/url_launcher.dart'; import '../../../utils/url_launcher.dart';
@ -19,6 +21,8 @@ class HtmlMessage extends StatelessWidget {
final double fontSize; final double fontSize;
final TextStyle linkStyle; final TextStyle linkStyle;
final void Function(LinkableElement) onOpen; final void Function(LinkableElement) onOpen;
final String? eventId;
final Set<Event>? checkboxCheckedEvents;
const HtmlMessage({ const HtmlMessage({
super.key, super.key,
@ -28,6 +32,8 @@ class HtmlMessage extends StatelessWidget {
required this.linkStyle, required this.linkStyle,
this.textColor = Colors.black, this.textColor = Colors.black,
required this.onOpen, required this.onOpen,
this.eventId,
this.checkboxCheckedEvents,
}); });
/// Keep in sync with: https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes /// Keep in sync with: https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes
@ -218,6 +224,24 @@ class HtmlMessage extends StatelessWidget {
if (!{'ol', 'ul'}.contains(node.parent?.localName)) { if (!{'ol', 'ul'}.contains(node.parent?.localName)) {
continue block; 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( return WidgetSpan(
child: Padding( child: Padding(
padding: EdgeInsets.only(left: fontSize), padding: EdgeInsets.only(left: fontSize),
@ -231,6 +255,42 @@ class HtmlMessage extends StatelessWidget {
text: text:
'${(node.parent?.nodes.whereType<dom.Element>().toList().indexOf(node) ?? 0) + (int.tryParse(node.parent?.attributes['start'] ?? '1') ?? 1)}. ', '${(node.parent?.nodes.whereType<dom.Element>().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( ..._renderWithLineBreaks(
node.nodes, node.nodes,
context, context,
@ -446,11 +506,9 @@ class HtmlMessage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final element = parser.parse(html).body ?? dom.Element.html('');
return Text.rich( return Text.rich(
_renderHtml( _renderHtml(element, context),
parser.parse(html).body ?? dom.Element.html(''),
context,
),
style: TextStyle( style: TextStyle(
fontSize: fontSize, fontSize: fontSize,
color: textColor, color: textColor,
@ -516,3 +574,7 @@ extension on String {
return colorValue == null ? null : Color(colorValue); return colorValue == null ? null : Color(colorValue);
} }
} }
extension on dom.Element {
dom.Element get rootElement => parent?.rootElement ?? this;
}

@ -13,6 +13,7 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import '../../../config/app_config.dart'; import '../../../config/app_config.dart';
import '../../../utils/event_checkbox_extension.dart';
import '../../../utils/platform_infos.dart'; import '../../../utils/platform_infos.dart';
import '../../../utils/url_launcher.dart'; import '../../../utils/url_launcher.dart';
import '../../bootstrap/bootstrap_dialog.dart'; import '../../bootstrap/bootstrap_dialog.dart';
@ -204,6 +205,11 @@ class MessageContent extends StatelessWidget {
decorationColor: linkColor, decorationColor: linkColor,
), ),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
eventId: event.eventId,
checkboxCheckedEvents: event.aggregatedEvents(
timeline,
EventCheckboxRoomExtension.relationshipType,
),
), ),
); );
} }

@ -0,0 +1,27 @@
import 'package:matrix/matrix.dart';
extension EventCheckboxRoomExtension on Room {
static const String relationshipType = 'im.fluffychat.checkboxes';
Future<String?> 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<String, Object?>('m.relates_to')
?.tryGet<int>('checkbox_id');
}

@ -1150,10 +1150,11 @@ packages:
matrix: matrix:
dependency: "direct main" dependency: "direct main"
description: description:
name: matrix path: "."
sha256: "7d15fdbc760be7e40c58bb65e03baa8241b1e31db2bc67dab61883aabc083a85" ref: "krille/add-markdown-checkboxes"
url: "https://pub.dev" resolved-ref: f3bb654ac2cda19bdd8a35fb46846018acd01a89
source: hosted url: "https://github.com/famedly/matrix-dart-sdk.git"
source: git
version: "0.40.0" version: "0.40.0"
meta: meta:
dependency: transitive dependency: transitive

@ -61,7 +61,10 @@ dependencies:
just_audio: ^0.9.39 just_audio: ^0.9.39
latlong2: ^0.9.1 latlong2: ^0.9.1
linkify: ^5.0.0 linkify: ^5.0.0
matrix: ^0.40.0 matrix:
git:
url: https://github.com/famedly/matrix-dart-sdk.git
ref: krille/add-markdown-checkboxes
mime: ^1.0.6 mime: ^1.0.6
native_imaging: ^0.2.0 native_imaging: ^0.2.0
opus_caf_converter_dart: ^1.0.1 opus_caf_converter_dart: ^1.0.1
@ -140,4 +143,4 @@ dependency_overrides:
url: https://github.com/ThexXTURBOXx/flutter_web_auth_2.git url: https://github.com/ThexXTURBOXx/flutter_web_auth_2.git
ref: 3.x-without-v1 ref: 3.x-without-v1
path: flutter_web_auth_2 path: flutter_web_auth_2
win32: 5.5.3 win32: 5.5.3
Loading…
Cancel
Save