feat(ui, core): handle ogAttachment modification via message_input_controller.dart.

Signed-off-by: xsahil03x <[email protected]>
This commit is contained in:
Sahil Kumar
2021-12-20 18:25:56 +05:30
committed by xsahil03x
parent 50e9cb38e9
commit 56e030f40c
3 changed files with 133 additions and 50 deletions
@@ -335,17 +335,6 @@ class MessageInput extends StatefulWidget {
@override @override
MessageInputState createState() => MessageInputState(); MessageInputState createState() => MessageInputState();
/// Use this method to get the current [StreamChatState] instance
static MessageInputState of(BuildContext context) {
MessageInputState? messageInputState;
messageInputState = context.findAncestorStateOfType<MessageInputState>();
assert(
messageInputState != null,
'You must have a MessageInput widget as ancestor of your widget tree',
);
return messageInputState!;
}
} }
/// State of [MessageInput] /// State of [MessageInput]
@@ -520,11 +509,11 @@ class MessageInputState extends State<MessageInput>
], ],
), ),
) )
else if (_ogAttachment != null) else if (_effectiveController.ogAttachment != null)
OGAttachmentPreview( OGAttachmentPreview(
attachment: _ogAttachment!, attachment: _effectiveController.ogAttachment!,
onDismissPreviewPressed: () { onDismissPreviewPressed: () {
setState(() => _ogAttachment = null); _effectiveController.clearOGAttachment();
_focusNode.unfocus(); _focusNode.unfocus();
}, },
), ),
@@ -929,7 +918,6 @@ class MessageInputState extends State<MessageInput>
return context.translations.writeAMessageLabel; return context.translations.writeAMessageLabel;
} }
Attachment? _ogAttachment;
String? _lastSearchedContainsUrlText; String? _lastSearchedContainsUrlText;
CancelableOperation? _enrichUrlOperation; CancelableOperation? _enrichUrlOperation;
@@ -948,14 +936,16 @@ class MessageInputState extends State<MessageInput>
// Reset the og attachment if the text doesn't contain any url // Reset the og attachment if the text doesn't contain any url
if (matchedUrls.isEmpty) { if (matchedUrls.isEmpty) {
setState(() => _ogAttachment = null); _effectiveController.clearOGAttachment();
return; return;
} }
final firstMatchedUrl = matchedUrls.first.group(0)!; final firstMatchedUrl = matchedUrls.first.group(0)!;
// If the parsed url matches the ogAttachment url, don't do anything // 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; final client = StreamChat.of(context).client;
@@ -964,11 +954,11 @@ class MessageInputState extends State<MessageInput>
).then( ).then(
(ogAttachment) { (ogAttachment) {
final attachment = Attachment.fromOGAttachment(ogAttachment); final attachment = Attachment.fromOGAttachment(ogAttachment);
setState(() => _ogAttachment = attachment); _effectiveController.setOGAttachment(attachment);
}, },
onError: (error, stackTrace) { onError: (error, stackTrace) {
// Reset the ogAttachment if there was an error // Reset the ogAttachment if there was an error
setState(() => _ogAttachment = null); _effectiveController.clearOGAttachment();
widget.onError?.call(error, stackTrace); widget.onError?.call(error, stackTrace);
}, },
); );
@@ -1470,14 +1460,6 @@ class MessageInputState extends State<MessageInput>
} }
} }
/// 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 /// Adds an attachment to the [messageInputController.attachments] map
void _addAttachments(Iterable<Attachment> attachments) { void _addAttachments(Iterable<Attachment> attachments) {
final limit = widget.attachmentLimit; final limit = widget.attachmentLimit;
@@ -1612,21 +1594,14 @@ class MessageInputState extends State<MessageInput>
/// Sends the current message /// Sends the current message
Future<void> sendMessage() async { Future<void> sendMessage() async {
var message = _effectiveController.value; final skipEnrichUrl = _effectiveController.ogAttachment == null;
// Add ogAttachment if present var message = _effectiveController.value;
final skipEnrichUrl = _ogAttachment == null;
if (!skipEnrichUrl) {
message = message.copyWith(
attachments: [...message.attachments, _ogAttachment!],
);
}
var shouldKeepFocus = widget.shouldKeepFocusAfterMessage; var shouldKeepFocus = widget.shouldKeepFocusAfterMessage;
shouldKeepFocus ??= !_commandEnabled; shouldKeepFocus ??= !_commandEnabled;
_ogAttachment = null;
_effectiveController.reset(); _effectiveController.reset();
if (widget.preMessageSending != null) { if (widget.preMessageSending != null) {
@@ -1,8 +1,9 @@
import 'dart:convert'; import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_chat/stream_chat.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]. /// A value listenable builder related to a [Message].
/// ///
@@ -17,33 +18,46 @@ class MessageInputController extends ValueNotifier<Message> {
/// message. /// message.
factory MessageInputController({ factory MessageInputController({
Message? message, Message? message,
Map<RegExp, TextStyle>? textPatternStyle,
}) => }) =>
MessageInputController._( MessageInputController._(
initialMessage: message ?? Message(), initialMessage: message ?? Message(),
textPatternStyle: textPatternStyle,
); );
/// Creates a controller for an editable text field from an initial [text]. /// Creates a controller for an editable text field from an initial [text].
factory MessageInputController.fromText(String? text) => factory MessageInputController.fromText(
String? text, {
Map<RegExp, TextStyle>? textPatternStyle,
}) =>
MessageInputController._( MessageInputController._(
initialMessage: Message(text: text), initialMessage: Message(text: text),
textPatternStyle: textPatternStyle,
); );
/// Creates a controller for an editable text field from initial /// Creates a controller for an editable text field from initial
/// [attachments]. /// [attachments].
factory MessageInputController.fromAttachments( factory MessageInputController.fromAttachments(
List<Attachment> attachments, List<Attachment> attachments, {
) => Map<RegExp, TextStyle>? textPatternStyle,
}) =>
MessageInputController._( MessageInputController._(
initialMessage: Message(attachments: attachments), initialMessage: Message(attachments: attachments),
textPatternStyle: textPatternStyle,
); );
MessageInputController._({ MessageInputController._({
required Message initialMessage, required Message initialMessage,
}) : _textEditingController = Map<RegExp, TextStyle>? textPatternStyle,
TextEditingController.fromValue(TextEditingValue( }) : _textEditingController = MessageTextFieldController.fromValue(
text: initialMessage.text ?? '', initialMessage.text == null
composing: TextRange.collapsed(initialMessage.text?.length ?? 0), ? const TextEditingValue()
)), : TextEditingValue(
text: initialMessage.text!,
composing: TextRange.collapsed(initialMessage.text!.length),
),
textPatternStyle: textPatternStyle,
),
_initialMessage = initialMessage, _initialMessage = initialMessage,
super(initialMessage) { super(initialMessage) {
addListener(_textEditingSyncer); addListener(_textEditingSyncer);
@@ -73,8 +87,9 @@ class MessageInputController extends ValueNotifier<Message> {
Message get message => value; Message get message => value;
/// Returns the controller of the text field linked to this controller. /// Returns the controller of the text field linked to this controller.
TextEditingController get textEditingController => _textEditingController; MessageTextFieldController get textEditingController =>
final TextEditingController _textEditingController; _textEditingController;
final MessageTextFieldController _textEditingController;
/// Returns the text of the message. /// Returns the text of the message.
String get text => _textEditingController.text; String get text => _textEditingController.text;
@@ -174,6 +189,28 @@ class MessageInputController extends ValueNotifier<Message> {
attachments = []; 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. /// Returns the list of mentioned users in the message.
List<User> get mentionedUsers => value.mentionedUsers; List<User> get mentionedUsers => value.mentionedUsers;
@@ -220,9 +257,8 @@ class MessageInputController extends ValueNotifier<Message> {
/// Sets the [value] to the initial [Message] value. /// Sets the [value] to the initial [Message] value.
void reset({bool resetId = true}) { void reset({bool resetId = true}) {
if (resetId) { if (resetId) {
_initialMessage = _initialMessage.copyWith( final newId = const Uuid().v4();
id: const Uuid().v4(), _initialMessage = _initialMessage.copyWith(id: newId);
);
} }
value = _initialMessage; value = _initialMessage;
} }
@@ -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<RegExp, TextStyle>? 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 = <TextSpan>[];
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);
}
}