From 56e030f40c83ab8cb86f9e0ac88ce657e72ececf Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 20 Dec 2021 18:25:56 +0530 Subject: [PATCH] feat(ui, core): handle ogAttachment modification via message_input_controller.dart. Signed-off-by: xsahil03x --- .../lib/src/message_input/message_input.dart | 47 +++--------- .../lib/src/message_input_controller.dart | 64 +++++++++++++---- .../src/message_text_field_controller.dart | 72 +++++++++++++++++++ 3 files changed, 133 insertions(+), 50 deletions(-) create mode 100644 packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index a28754d3..7e02e4b3 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -335,17 +335,6 @@ class MessageInput extends StatefulWidget { @override MessageInputState createState() => MessageInputState(); - - /// Use this method to get the current [StreamChatState] instance - static MessageInputState of(BuildContext context) { - MessageInputState? messageInputState; - messageInputState = context.findAncestorStateOfType(); - assert( - messageInputState != null, - 'You must have a MessageInput widget as ancestor of your widget tree', - ); - return messageInputState!; - } } /// State of [MessageInput] @@ -520,11 +509,11 @@ class MessageInputState extends State ], ), ) - else if (_ogAttachment != null) + else if (_effectiveController.ogAttachment != null) OGAttachmentPreview( - attachment: _ogAttachment!, + attachment: _effectiveController.ogAttachment!, onDismissPreviewPressed: () { - setState(() => _ogAttachment = null); + _effectiveController.clearOGAttachment(); _focusNode.unfocus(); }, ), @@ -929,7 +918,6 @@ class MessageInputState extends State return context.translations.writeAMessageLabel; } - Attachment? _ogAttachment; String? _lastSearchedContainsUrlText; CancelableOperation? _enrichUrlOperation; @@ -948,14 +936,16 @@ class MessageInputState extends State // Reset the og attachment if the text doesn't contain any url if (matchedUrls.isEmpty) { - setState(() => _ogAttachment = null); + _effectiveController.clearOGAttachment(); return; } final firstMatchedUrl = matchedUrls.first.group(0)!; // If the parsed url matches the ogAttachment url, don't do anything - if (_ogAttachment?.titleLink == firstMatchedUrl) return; + if (_effectiveController.ogAttachment?.titleLink == firstMatchedUrl) { + return; + } final client = StreamChat.of(context).client; @@ -964,11 +954,11 @@ class MessageInputState extends State ).then( (ogAttachment) { final attachment = Attachment.fromOGAttachment(ogAttachment); - setState(() => _ogAttachment = attachment); + _effectiveController.setOGAttachment(attachment); }, onError: (error, stackTrace) { // Reset the ogAttachment if there was an error - setState(() => _ogAttachment = null); + _effectiveController.clearOGAttachment(); widget.onError?.call(error, stackTrace); }, ); @@ -1470,14 +1460,6 @@ class MessageInputState extends State } } - /// Add an attachment to the sending message - /// Use this to add custom type attachments - /// - /// Note: Only meant to be used from outside the state. - void addAttachment(Attachment attachment) { - _addAttachments([attachment]); - } - /// Adds an attachment to the [messageInputController.attachments] map void _addAttachments(Iterable attachments) { final limit = widget.attachmentLimit; @@ -1612,21 +1594,14 @@ class MessageInputState extends State /// Sends the current message Future sendMessage() async { - var message = _effectiveController.value; + final skipEnrichUrl = _effectiveController.ogAttachment == null; - // Add ogAttachment if present - final skipEnrichUrl = _ogAttachment == null; - if (!skipEnrichUrl) { - message = message.copyWith( - attachments: [...message.attachments, _ogAttachment!], - ); - } + var message = _effectiveController.value; var shouldKeepFocus = widget.shouldKeepFocusAfterMessage; shouldKeepFocus ??= !_commandEnabled; - _ogAttachment = null; _effectiveController.reset(); if (widget.preMessageSending != null) { diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart index 29bd2db2..f218b9ca 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -1,8 +1,9 @@ import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/src/message_text_field_controller.dart'; /// A value listenable builder related to a [Message]. /// @@ -17,33 +18,46 @@ class MessageInputController extends ValueNotifier { /// message. factory MessageInputController({ Message? message, + Map? textPatternStyle, }) => MessageInputController._( initialMessage: message ?? Message(), + textPatternStyle: textPatternStyle, ); /// Creates a controller for an editable text field from an initial [text]. - factory MessageInputController.fromText(String? text) => + factory MessageInputController.fromText( + String? text, { + Map? textPatternStyle, + }) => MessageInputController._( initialMessage: Message(text: text), + textPatternStyle: textPatternStyle, ); /// Creates a controller for an editable text field from initial /// [attachments]. factory MessageInputController.fromAttachments( - List attachments, - ) => + List attachments, { + Map? textPatternStyle, + }) => MessageInputController._( initialMessage: Message(attachments: attachments), + textPatternStyle: textPatternStyle, ); MessageInputController._({ required Message initialMessage, - }) : _textEditingController = - TextEditingController.fromValue(TextEditingValue( - text: initialMessage.text ?? '', - composing: TextRange.collapsed(initialMessage.text?.length ?? 0), - )), + Map? textPatternStyle, + }) : _textEditingController = MessageTextFieldController.fromValue( + initialMessage.text == null + ? const TextEditingValue() + : TextEditingValue( + text: initialMessage.text!, + composing: TextRange.collapsed(initialMessage.text!.length), + ), + textPatternStyle: textPatternStyle, + ), _initialMessage = initialMessage, super(initialMessage) { addListener(_textEditingSyncer); @@ -73,8 +87,9 @@ class MessageInputController extends ValueNotifier { Message get message => value; /// Returns the controller of the text field linked to this controller. - TextEditingController get textEditingController => _textEditingController; - final TextEditingController _textEditingController; + MessageTextFieldController get textEditingController => + _textEditingController; + final MessageTextFieldController _textEditingController; /// Returns the text of the message. String get text => _textEditingController.text; @@ -174,6 +189,28 @@ class MessageInputController extends ValueNotifier { attachments = []; } + // Only used to store the value locally in order to remove it if we call + // [clearOGAttachment] or [setOGAttachment] again. + Attachment? _ogAttachment; + + /// Returns the og attachment of the message if set + Attachment? get ogAttachment => + attachments.firstWhereOrNull((it) => it.id == _ogAttachment?.id); + + /// Sets the og attachment in the message. + void setOGAttachment(Attachment attachment) { + attachments = [...attachments] + ..remove(_ogAttachment) + ..insert(0, attachment); + _ogAttachment = attachment; + } + + /// Removes the og attachment. + void clearOGAttachment() { + attachments = [...attachments]..remove(_ogAttachment); + _ogAttachment = null; + } + /// Returns the list of mentioned users in the message. List get mentionedUsers => value.mentionedUsers; @@ -220,9 +257,8 @@ class MessageInputController extends ValueNotifier { /// Sets the [value] to the initial [Message] value. void reset({bool resetId = true}) { if (resetId) { - _initialMessage = _initialMessage.copyWith( - id: const Uuid().v4(), - ); + final newId = const Uuid().v4(); + _initialMessage = _initialMessage.copyWith(id: newId); } value = _initialMessage; } diff --git a/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart new file mode 100644 index 00000000..82a60709 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +class MessageTextFieldController extends TextEditingController { + MessageTextFieldController({ + String? text, + this.textPatternStyle, + }) : super(text: text); + + MessageTextFieldController.fromValue( + TextEditingValue? value, { + this.textPatternStyle, + }) : super.fromValue(value); + + final Map? textPatternStyle; + + /// + @override + TextSpan buildTextSpan({ + required BuildContext context, + TextStyle? style, + required bool withComposing, + }) { + final pattern = textPatternStyle; + if (pattern == null) { + return super.buildTextSpan( + context: context, + style: style, + withComposing: withComposing, + ); + } + + return TextSpan(text: text, style: style).splitMapJoin( + RegExp(pattern.keys.map((it) => it.pattern).join('|')), + onMatch: (match) { + final text = match[0]!; + final key = pattern.keys.firstWhere((it) => it.hasMatch(text)); + return TextSpan( + text: text, + style: pattern[key], + ); + }, + ); + } +} + +extension _TextSpanX on TextSpan { + TextSpan splitMapJoin( + Pattern pattern, { + TextSpan Function(Match)? onMatch, + TextSpan Function(TextSpan)? onNonMatch, + }) { + final children = []; + + toPlainText().splitMapJoin( + pattern, + onMatch: (match) { + final span = TextSpan(text: match.group(0), style: style); + final updated = onMatch?.call(match); + children.add(updated ?? span); + return span.toPlainText(); + }, + onNonMatch: (text) { + final span = TextSpan(text: text, style: style); + final updatedSpan = onNonMatch?.call(span); + children.add(updatedSpan ?? span); + return span.toPlainText(); + }, + ); + + return TextSpan(style: style, children: children); + } +}