move message input controllers to ui
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user