Last active
October 25, 2021 12:03
-
-
Save xsahil03x/c554330517002125b502d0101ed0d3ff to your computer and use it in GitHub Desktop.
StreamMessageTextField
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:convert'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/widgets.dart'; | |
import 'package:stream_chat/stream_chat.dart'; | |
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}) => | |
MessageInputController._(message ?? Message()); | |
/// Creates a controller for an editable text field from an initial [text]. | |
factory MessageInputController.fromText(String? text) => | |
MessageInputController._(Message(text: text)); | |
/// Creates a controller for an editable text field from an initial [attachments]. | |
factory MessageInputController.fromAttachments( | |
List<Attachment> attachments, | |
) => | |
MessageInputController._(Message(attachments: attachments)); | |
MessageInputController._(Message message) | |
: _textEditingController = TextEditingController(text: message.text), | |
super(message) { | |
_textEditingController.addListener(_textEditingListener); | |
} | |
void _textEditingListener() { | |
final newText = _textEditingController.text; | |
value = value.copyWith(text: newText); | |
} | |
/// | |
TextEditingController get textEditingController => _textEditingController; | |
final TextEditingController _textEditingController; | |
/// | |
String get text => _textEditingController.text; | |
set text(String newText) { | |
_textEditingController.text = newText; | |
} | |
/// | |
List<Attachment> get attachments => value.attachments; | |
set attachments(List<Attachment> attachments) { | |
value = value.copyWith(attachments: attachments); | |
} | |
/// | |
void addAttachment(Attachment attachment) { | |
attachments = [...attachments, attachment]; | |
} | |
/// | |
void addAttachmentAt(int index, Attachment attachment) { | |
attachments = [...attachments]..insert(index, attachment); | |
} | |
/// | |
void removeAttachment(Attachment attachment) { | |
attachments = [...attachments]..remove(attachment); | |
} | |
/// | |
void removeAttachmentById(String attachmentId) { | |
attachments = [...attachments]..removeWhere((it) => it.id == attachmentId); | |
} | |
/// | |
void removeAttachmentAt(int index) { | |
attachments = [...attachments]..removeAt(index); | |
} | |
/// | |
List<User> get mentionedUsers => value.mentionedUsers; | |
set mentionedUsers(List<User> users) { | |
value = value.copyWith(mentionedUsers: users); | |
} | |
/// | |
void addMentionedUser(User user) { | |
mentionedUsers = [...mentionedUsers, user]; | |
} | |
/// | |
void removeMentionedUser(User user) { | |
mentionedUsers = [...mentionedUsers]..remove(user); | |
} | |
/// | |
void removeMentionedUserById(String userId) { | |
mentionedUsers = [...mentionedUsers]..removeWhere((it) => it.id == userId); | |
} | |
/// Set the [value] to empty. | |
/// | |
/// After calling this function, [text], [attachments] and [mentionedUsers] | |
/// all will be empty. | |
/// | |
/// Calling this will notify all the listeners of this [MessageInputController] | |
/// that they need to update (it 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(); | |
} | |
@override | |
void dispose() { | |
_textEditingController.removeListener(_textEditingListener); | |
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 treats a null `text` argument as if it were the empty | |
/// string. | |
RestorableMessageInputController({Message? message}) | |
: _initialValue = message ?? Message(); | |
/// Creates a [RestorableMessageInputController] from an initial | |
/// [TextEditingValue]. | |
/// | |
/// This constructor treats a null `value` argument as if it were | |
/// [TextEditingValue.empty]. | |
factory RestorableMessageInputController.fromText(String? text) => | |
RestorableMessageInputController(message: Message(text: text)); | |
final Message _initialValue; | |
@override | |
MessageInputController createDefaultValue() { | |
return 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); | |
} | |
class StreamMessageTextField extends StatefulWidget { | |
const StreamMessageTextField({ | |
Key? key, | |
this.controller, | |
this.restorationId, | |
this.focusNode, | |
this.decoration = const InputDecoration(), | |
}) : super(key: key); | |
/// Controls the message being edited. | |
/// | |
/// If null, this widget will create its own [MessageInputController]. | |
final MessageInputController? controller; | |
/// Defines the keyboard focus for this widget. | |
/// | |
/// The [focusNode] is a long-lived object that's typically managed by a | |
/// [StatefulWidget] parent. See [FocusNode] for more information. | |
/// | |
/// To give the keyboard focus to this widget, provide a [focusNode] and then | |
/// use the current [FocusScope] to request the focus: | |
/// | |
/// ```dart | |
/// FocusScope.of(context).requestFocus(myFocusNode); | |
/// ``` | |
/// | |
/// This happens automatically when the widget is tapped. | |
/// | |
/// To be notified when the widget gains or loses the focus, add a listener | |
/// to the [focusNode]: | |
/// | |
/// ```dart | |
/// focusNode.addListener(() { print(myFocusNode.hasFocus); }); | |
/// ``` | |
/// | |
/// If null, this widget will create its own [FocusNode]. | |
/// | |
/// ## Keyboard | |
/// | |
/// Requesting the focus will typically cause the keyboard to be shown | |
/// if it's not showing already. | |
/// | |
/// On Android, the user can hide the keyboard - without changing the focus - | |
/// with the system back button. They can restore the keyboard's visibility | |
/// by tapping on a text field. The user might hide the keyboard and | |
/// switch to a physical keyboard, or they might just need to get it | |
/// out of the way for a moment, to expose something it's | |
/// obscuring. In this case requesting the focus again will not | |
/// cause the focus to change, and will not make the keyboard visible. | |
/// | |
/// This widget builds an [EditableText] and will ensure that the keyboard is | |
/// showing when it is tapped by calling [EditableTextState.requestKeyboard()]. | |
final FocusNode? focusNode; | |
/// The decoration to show around the text field. | |
/// | |
/// By default, draws a horizontal line under the text field but can be | |
/// configured to show an icon, label, hint text, and error text. | |
/// | |
/// Specify null to remove the decoration entirely (including the | |
/// extra padding introduced by the decoration to save space for the labels). | |
final InputDecoration? decoration; | |
/// Restoration ID to save and restore the state of the text field. | |
/// | |
/// If non-null, the text field will persist and restore its current scroll | |
/// offset and - if no [controller] has been provided - the content of the | |
/// text field. If a [controller] has been provided, it is the responsibility | |
/// of the owner of that controller to persist and restore it, e.g. by using | |
/// a [RestorableTextEditingController]. | |
/// | |
/// The state of this widget is persisted in a [RestorationBucket] claimed | |
/// from the surrounding [RestorationScope] using the provided restoration ID. | |
/// | |
/// See also: | |
/// | |
/// * [RestorationManager], which explains how state restoration works in | |
/// Flutter. | |
final String? restorationId; | |
@override | |
_StreamMessageTextFieldState createState() => _StreamMessageTextFieldState(); | |
} | |
class _StreamMessageTextFieldState extends State<StreamMessageTextField> | |
with RestorationMixin<StreamMessageTextField> { | |
RestorableMessageInputController? _controller; | |
MessageInputController get _effectiveController => | |
widget.controller ?? _controller!.value; | |
@override | |
void initState() { | |
super.initState(); | |
if (widget.controller == null) { | |
_createLocalController(); | |
} | |
} | |
void _createLocalController([Message? message]) { | |
assert(_controller == null, ''); | |
_controller = RestorableMessageInputController(message: message); | |
} | |
@override | |
void didUpdateWidget(covariant StreamMessageTextField oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
if (widget.controller == null && oldWidget.controller != null) { | |
_createLocalController(oldWidget.controller!.value); | |
} else if (widget.controller != null && oldWidget.controller == null) { | |
unregisterFromRestoration(_controller!); | |
_controller!.dispose(); | |
_controller = null; | |
} | |
} | |
@override | |
void restoreState(RestorationBucket? oldBucket, bool initialRestore) { | |
if (_controller != null) { | |
_registerController(); | |
} | |
} | |
@override | |
String? get restorationId => widget.restorationId; | |
void _registerController() { | |
assert(_controller != null, ''); | |
registerForRestoration(_controller!, 'controller'); | |
} | |
late final _onChangedDebounced = debounce( | |
(String newText) => _effectiveController.text = newText, | |
const Duration(milliseconds: 350), | |
leading: true, | |
); | |
@override | |
Widget build(BuildContext context) => TextField( | |
focusNode: widget.focusNode, | |
decoration: widget.decoration, | |
controller: _effectiveController.textEditingController, | |
onChanged: (newText) => _onChangedDebounced([newText]), | |
); | |
@override | |
void dispose() { | |
_onChangedDebounced.cancel(); | |
_controller?.dispose(); | |
super.dispose(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage:
channel.sendMessage(controller.value)