move message input controllers to ui

This commit is contained in:
Salvatore Giordano
2021-12-29 15:19:27 +01:00
parent 8278e1f745
commit e85b1a8b3f
6 changed files with 5 additions and 5 deletions
@@ -0,0 +1,307 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A value listenable builder related to a [Message].
///
/// Pass in a [MessageInputController] as the `valueListenable`.
typedef MessageValueListenableBuilder = ValueListenableBuilder<Message>;
/// Controller for storing and mutating a [Message] value.
class MessageInputController extends ValueNotifier<Message> {
/// Creates a controller for an editable text field.
///
/// This constructor treats a null [message] argument as if it were the empty
/// message.
factory MessageInputController({
Message? message,
Map<RegExp, TextStyle>? textPatternStyle,
}) =>
MessageInputController._(
initialMessage: message ?? Message(),
textPatternStyle: textPatternStyle,
);
/// Creates a controller for an editable text field from an initial [text].
factory MessageInputController.fromText(
String? text, {
Map<RegExp, TextStyle>? textPatternStyle,
}) =>
MessageInputController._(
initialMessage: Message(text: text),
textPatternStyle: textPatternStyle,
);
/// Creates a controller for an editable text field from initial
/// [attachments].
factory MessageInputController.fromAttachments(
List<Attachment> attachments, {
Map<RegExp, TextStyle>? textPatternStyle,
}) =>
MessageInputController._(
initialMessage: Message(attachments: attachments),
textPatternStyle: textPatternStyle,
);
MessageInputController._({
required Message initialMessage,
Map<RegExp, TextStyle>? 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);
}
void _textEditingSyncer() {
final cleanText = value.command == null
? value.text
: value.text?.replaceFirst('/${value.command} ', '');
if (cleanText != _textEditingController.text) {
final previousOffset = _textEditingController.value.selection.start;
final previousText = _textEditingController.text;
final diff = (cleanText?.length ?? 0) - previousText.length;
_textEditingController
..text = cleanText ?? ''
..selection = TextSelection.collapsed(
offset: previousOffset + diff,
);
}
}
/// Returns the current message associated with this controller.
Message get message => value;
/// Returns the controller of the text field linked to this controller.
MessageTextFieldController get textEditingController =>
_textEditingController;
final MessageTextFieldController _textEditingController;
/// Returns the text of the message.
String get text => _textEditingController.text;
Message _initialMessage;
/// Sets the message.
set message(Message message) {
value = message;
}
/// Sets the message that's being quoted.
set quotedMessage(Message message) {
value = value.copyWith(
quotedMessage: message,
quotedMessageId: message.id,
);
}
/// Clears the quoted message.
void clearQuotedMessage() {
value = value.copyWith(
quotedMessageId: null,
quotedMessage: null,
);
}
/// Sets a command for the message.
set command(Command command) {
value = value.copyWith(
command: command.name,
text: '/${command.name} ',
);
}
/// Sets the text of the message.
set text(String newText) {
var newTextWithCommand = newText;
if (value.command != null) {
if (!newText.startsWith('/${value.command}')) {
newTextWithCommand = '/${value.command} $newText';
}
}
value = value.copyWith(text: newTextWithCommand);
}
/// Returns the baseOffset of the text field.
int get baseOffset => textEditingController.selection.baseOffset;
/// Returns the start of the selection of the text field.
int get selectionStart => textEditingController.selection.start;
/// Sets the [showInChannel] flag of the message.
set showInChannel(bool newValue) {
value = value.copyWith(showInChannel: newValue);
}
/// Returns true if the message is in a thread and
/// should be shown in the main channel as well.
bool get showInChannel => value.showInChannel ?? false;
/// Returns the attachments of the message.
List<Attachment> get attachments => value.attachments;
/// Sets the list of [attachments] for the message.
set attachments(List<Attachment> attachments) {
value = value.copyWith(attachments: attachments);
}
/// Adds a new attachment to the message.
void addAttachment(Attachment attachment) {
attachments = [...attachments, attachment];
}
/// Adds a new attachment at the specified [index].
void addAttachmentAt(int index, Attachment attachment) {
attachments = [...attachments]..insert(index, attachment);
}
/// Removes the specified [attachment] from the message.
void removeAttachment(Attachment attachment) {
attachments = [...attachments]..remove(attachment);
}
/// Remove the attachment with the given [attachmentId].
void removeAttachmentById(String attachmentId) {
attachments = [...attachments]..removeWhere((it) => it.id == attachmentId);
}
/// Removes the attachment at the given [index].
void removeAttachmentAt(int index) {
attachments = [...attachments]..removeAt(index);
}
/// Clears the message attachments.
void clearAttachments() {
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() {
if (_ogAttachment != null) {
removeAttachment(_ogAttachment!);
}
_ogAttachment = null;
}
/// Returns the list of mentioned users in the message.
List<User> get mentionedUsers => value.mentionedUsers;
/// Sets the mentioned users.
set mentionedUsers(List<User> users) {
value = value.copyWith(mentionedUsers: users);
}
/// Adds a user to the list of mentioned users.
void addMentionedUser(User user) {
mentionedUsers = [...mentionedUsers, user];
}
/// Removes the specified [user] from the mentioned users list.
void removeMentionedUser(User user) {
mentionedUsers = [...mentionedUsers]..remove(user);
}
/// Removes the mentioned user with the given [userId].
void removeMentionedUserById(String userId) {
mentionedUsers = [...mentionedUsers]..removeWhere((it) => it.id == userId);
}
/// Removes all mentioned users from the message.
void clearMentionedUsers() {
mentionedUsers = [];
}
/// Sets the [message], or [value], to empty.
///
/// After calling this function, [text], [attachments] and [mentionedUsers]
/// will all be empty.
///
/// Calling this will notify all the listeners of this
/// [MessageInputController] that they need to update
/// (calls [notifyListeners]). For this reason,
/// this method should only be called between frames, e.g. in response to user
/// actions, not during the build, layout, or paint phases.
void clear() {
value = Message();
_textEditingController.clear();
}
/// Sets the [value] to the initial [Message] value.
void reset({bool resetId = true}) {
if (resetId) {
final newId = const Uuid().v4();
_initialMessage = _initialMessage.copyWith(id: newId);
}
value = _initialMessage;
}
@override
void dispose() {
removeListener(_textEditingSyncer);
_textEditingController.dispose();
super.dispose();
}
}
/// A [RestorableProperty] that knows how to store and restore a
/// [MessageInputController].
///
/// The [MessageInputController] is accessible via the [value] getter. During
/// state restoration, the property will restore [MessageInputController.value]
/// to the value it had when the restoration data it is getting restored from
/// was collected.
class RestorableMessageInputController
extends RestorableChangeNotifier<MessageInputController> {
/// Creates a [RestorableMessageInputController].
///
/// This constructor creates a default [Message] when no `message` argument
/// is supplied.
RestorableMessageInputController({Message? message})
: _initialValue = message ?? Message();
/// Creates a [RestorableMessageInputController] from an initial
/// [text] value.
factory RestorableMessageInputController.fromText(String? text) =>
RestorableMessageInputController(message: Message(text: text));
final Message _initialValue;
@override
MessageInputController createDefaultValue() =>
MessageInputController(message: _initialValue);
@override
MessageInputController fromPrimitives(Object? data) {
final message = Message.fromJson(json.decode(data! as String));
return MessageInputController(message: message);
}
@override
String toPrimitives() => json.encode(value.value);
}
@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
/// Controller for the [StreamTextField] widget.
class MessageTextFieldController extends TextEditingController {
/// Returns a new MessageTextFieldController
MessageTextFieldController({
String? text,
this.textPatternStyle,
}) : super(text: text);
/// Returns a new MessageTextFieldController with the given text [value].
MessageTextFieldController.fromValue(
TextEditingValue? value, {
this.textPatternStyle,
}) : super.fromValue(value);
/// A map of style to apply to the text matching the RegExp patterns.
final Map<RegExp, TextStyle>? textPatternStyle;
/// Builds a [TextSpan] from the current text,
/// highlighting the matches for [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);
}
}
@@ -8,7 +8,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
export 'package:flutter/services.dart'
show