added stream message button, general refactor
This commit is contained in:
@@ -79,6 +79,11 @@ typedef ActionButtonBuilder = Widget Function(
|
|||||||
IconButton defaultActionButton,
|
IconButton defaultActionButton,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
typedef AttachmentsPickerBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
List<Attachment> attachments,
|
||||||
|
);
|
||||||
|
|
||||||
/// Location for actions on the [MessageInput]
|
/// Location for actions on the [MessageInput]
|
||||||
enum ActionsLocation {
|
enum ActionsLocation {
|
||||||
/// Align to left
|
/// Align to left
|
||||||
@@ -204,6 +209,7 @@ class MessageInput extends StatefulWidget {
|
|||||||
this.commandButtonBuilder,
|
this.commandButtonBuilder,
|
||||||
this.customOverlays = const [],
|
this.customOverlays = const [],
|
||||||
this.mentionAllAppUsers = false,
|
this.mentionAllAppUsers = false,
|
||||||
|
this.attachmentsPickerBuilder,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
initialMessage == null || editMessage == null,
|
initialMessage == null || editMessage == null,
|
||||||
"Can't provide both `initialMessage` and `editMessage`",
|
"Can't provide both `initialMessage` and `editMessage`",
|
||||||
@@ -322,6 +328,9 @@ class MessageInput extends StatefulWidget {
|
|||||||
/// Defaults to false.
|
/// Defaults to false.
|
||||||
final bool mentionAllAppUsers;
|
final bool mentionAllAppUsers;
|
||||||
|
|
||||||
|
/// Builds bottom sheet when attachment picker is opened.
|
||||||
|
final AttachmentsPickerBuilder? attachmentsPickerBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MessageInputState createState() => MessageInputState();
|
MessageInputState createState() => MessageInputState();
|
||||||
|
|
||||||
@@ -524,7 +533,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
widget.actionsLocation == ActionsLocation.right)
|
widget.actionsLocation == ActionsLocation.right)
|
||||||
_buildExpandActionsButton(context),
|
_buildExpandActionsButton(context),
|
||||||
if (widget.sendButtonLocation == SendButtonLocation.outside)
|
if (widget.sendButtonLocation == SendButtonLocation.outside)
|
||||||
_animateSendButton(context),
|
_buildSendButton(context),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -588,27 +597,15 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _animateSendButton(BuildContext context) {
|
Widget _buildSendButton(BuildContext context) => StreamMessageSendButton(
|
||||||
late Widget sendButton;
|
onSendMessage: sendMessage,
|
||||||
if (_timeOut > 0) {
|
timeOut: _timeOut,
|
||||||
sendButton = _CountdownButton(count: _timeOut);
|
isIdle:
|
||||||
} else if (!_messageIsPresent &&
|
!_messageIsPresent && messageInputController.attachments.isEmpty,
|
||||||
messageInputController.attachments.isEmpty) {
|
isEditEnabled: widget.editMessage != null,
|
||||||
sendButton = widget.idleSendButton ?? _buildIdleSendButton(context);
|
idleSendButton: widget.idleSendButton,
|
||||||
} else {
|
activeSendButton: widget.activeSendButton,
|
||||||
sendButton = widget.activeSendButton != null
|
);
|
||||||
? InkWell(
|
|
||||||
onTap: sendMessage,
|
|
||||||
child: widget.activeSendButton,
|
|
||||||
)
|
|
||||||
: _buildSendButton(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
return AnimatedSwitcher(
|
|
||||||
duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!,
|
|
||||||
child: sendButton,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildExpandActionsButton(BuildContext context) {
|
Widget _buildExpandActionsButton(BuildContext context) {
|
||||||
final channel = StreamChannel.of(context).channel;
|
final channel = StreamChannel.of(context).channel;
|
||||||
@@ -817,7 +814,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
widget.actionsLocation == ActionsLocation.rightInside)
|
widget.actionsLocation == ActionsLocation.rightInside)
|
||||||
_buildExpandActionsButton(context),
|
_buildExpandActionsButton(context),
|
||||||
if (widget.sendButtonLocation == SendButtonLocation.inside)
|
if (widget.sendButtonLocation == SendButtonLocation.inside)
|
||||||
_animateSendButton(context),
|
_buildSendButton(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
).merge(passedDecoration);
|
).merge(passedDecoration);
|
||||||
@@ -993,6 +990,13 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_openFilePickerSection && widget.attachmentsPickerBuilder != null) {
|
||||||
|
return widget.attachmentsPickerBuilder!(
|
||||||
|
context,
|
||||||
|
messageInputController.attachments,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return AnimatedContainer(
|
return AnimatedContainer(
|
||||||
duration: _openFilePickerSection
|
duration: _openFilePickerSection
|
||||||
? const Duration(milliseconds: 300)
|
? const Duration(milliseconds: 300)
|
||||||
@@ -1736,49 +1740,6 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildIdleSendButton(BuildContext context) => Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: StreamSvgIcon(
|
|
||||||
assetName: _getIdleSendIcon(),
|
|
||||||
color: _messageInputTheme.sendButtonIdleColor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildSendButton(BuildContext context) => Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: sendMessage,
|
|
||||||
padding: const EdgeInsets.all(0),
|
|
||||||
splashRadius: 24,
|
|
||||||
constraints: const BoxConstraints.tightFor(
|
|
||||||
height: 24,
|
|
||||||
width: 24,
|
|
||||||
),
|
|
||||||
icon: StreamSvgIcon(
|
|
||||||
assetName: _getSendIcon(),
|
|
||||||
color: _messageInputTheme.sendButtonColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
String _getIdleSendIcon() {
|
|
||||||
if (_commandEnabled) {
|
|
||||||
return 'Icon_search.svg';
|
|
||||||
} else {
|
|
||||||
return 'Icon_circle_right.svg';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getSendIcon() {
|
|
||||||
if (widget.editMessage != null) {
|
|
||||||
return 'Icon_circle_up.svg';
|
|
||||||
} else if (_commandEnabled) {
|
|
||||||
return 'Icon_search.svg';
|
|
||||||
} else {
|
|
||||||
return 'Icon_circle_up.svg';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends the current message
|
/// Sends the current message
|
||||||
Future<void> sendMessage() async {
|
Future<void> sendMessage() async {
|
||||||
var text = messageInputController.text.trim();
|
var text = messageInputController.text.trim();
|
||||||
@@ -2080,30 +2041,3 @@ class _PickerWidgetState extends State<_PickerWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CountdownButton extends StatelessWidget {
|
|
||||||
const _CountdownButton({
|
|
||||||
Key? key,
|
|
||||||
required this.count,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final int count;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: StreamChatTheme.of(context).colorTheme.disabled,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: SizedBox(
|
|
||||||
height: 24,
|
|
||||||
width: 24,
|
|
||||||
child: Center(
|
|
||||||
child: Text('$count'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
class CountdownButton extends StatelessWidget {
|
||||||
|
const CountdownButton({
|
||||||
|
Key? key,
|
||||||
|
required this.count,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: StreamChatTheme.of(context).colorTheme.disabled,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
child: Center(
|
||||||
|
child: Text('$count'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1 +1,98 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
class StreamMessageSendButton extends StatelessWidget {
|
||||||
|
final int timeOut;
|
||||||
|
final bool isIdle;
|
||||||
|
final bool isCommandEnabled;
|
||||||
|
final bool isEditEnabled;
|
||||||
|
final Widget? idleSendButton;
|
||||||
|
final Widget? activeSendButton;
|
||||||
|
final VoidCallback onSendMessage;
|
||||||
|
|
||||||
|
const StreamMessageSendButton({
|
||||||
|
Key? key,
|
||||||
|
this.timeOut = 0,
|
||||||
|
this.isIdle = true,
|
||||||
|
this.isCommandEnabled = false,
|
||||||
|
this.isEditEnabled = false,
|
||||||
|
this.idleSendButton,
|
||||||
|
this.activeSendButton,
|
||||||
|
required this.onSendMessage,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var _streamChatTheme = StreamChatTheme.of(context);
|
||||||
|
|
||||||
|
late Widget sendButton;
|
||||||
|
if (timeOut > 0) {
|
||||||
|
sendButton = CountdownButton(count: timeOut);
|
||||||
|
} else if (isIdle) {
|
||||||
|
sendButton = idleSendButton ?? _buildIdleSendButton(context);
|
||||||
|
} else {
|
||||||
|
sendButton = activeSendButton != null
|
||||||
|
? InkWell(
|
||||||
|
onTap: onSendMessage,
|
||||||
|
child: activeSendButton,
|
||||||
|
)
|
||||||
|
: _buildSendButton(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!,
|
||||||
|
child: sendButton,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildIdleSendButton(BuildContext context) {
|
||||||
|
var _messageInputTheme = MessageInputTheme.of(context);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: StreamSvgIcon(
|
||||||
|
assetName: _getIdleSendIcon(),
|
||||||
|
color: _messageInputTheme.sendButtonIdleColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSendButton(BuildContext context) {
|
||||||
|
var _messageInputTheme = MessageInputTheme.of(context);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: onSendMessage,
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
splashRadius: 24,
|
||||||
|
constraints: const BoxConstraints.tightFor(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
),
|
||||||
|
icon: StreamSvgIcon(
|
||||||
|
assetName: _getSendIcon(),
|
||||||
|
color: _messageInputTheme.sendButtonColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getIdleSendIcon() {
|
||||||
|
if (isCommandEnabled) {
|
||||||
|
return 'Icon_search.svg';
|
||||||
|
} else {
|
||||||
|
return 'Icon_circle_right.svg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getSendIcon() {
|
||||||
|
if (isEditEnabled) {
|
||||||
|
return 'Icon_circle_up.svg';
|
||||||
|
} else if (isCommandEnabled) {
|
||||||
|
return 'Icon_search.svg';
|
||||||
|
} else {
|
||||||
|
return 'Icon_circle_up.svg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export 'src/message_search_item.dart';
|
|||||||
export 'src/message_search_list_view.dart';
|
export 'src/message_search_list_view.dart';
|
||||||
export 'src/message_text.dart';
|
export 'src/message_text.dart';
|
||||||
export 'src/message_widget.dart';
|
export 'src/message_widget.dart';
|
||||||
|
export 'src/mip/countdown_button.dart';
|
||||||
|
export 'src/mip/stream_message_send_button.dart';
|
||||||
export 'src/mip/stream_message_text_field.dart';
|
export 'src/mip/stream_message_text_field.dart';
|
||||||
export 'src/option_list_tile.dart';
|
export 'src/option_list_tile.dart';
|
||||||
export 'src/reaction_icon.dart';
|
export 'src/reaction_icon.dart';
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
|
/// Controller for storing and mutating a [Message] value.
|
||||||
class MessageInputController extends ValueNotifier<Message> {
|
class MessageInputController extends ValueNotifier<Message> {
|
||||||
/// Creates a controller for an editable text field.
|
/// Creates a controller for an editable text field.
|
||||||
///
|
///
|
||||||
@@ -17,7 +17,8 @@ class MessageInputController extends ValueNotifier<Message> {
|
|||||||
factory MessageInputController.fromText(String? text) =>
|
factory MessageInputController.fromText(String? text) =>
|
||||||
MessageInputController._(Message(text: text));
|
MessageInputController._(Message(text: text));
|
||||||
|
|
||||||
/// Creates a controller for an editable text field from an initial [attachments].
|
/// Creates a controller for an editable text field from an initial
|
||||||
|
/// [attachments].
|
||||||
factory MessageInputController.fromAttachments(
|
factory MessageInputController.fromAttachments(
|
||||||
List<Attachment> attachments,
|
List<Attachment> attachments,
|
||||||
) =>
|
) =>
|
||||||
@@ -55,23 +56,17 @@ class MessageInputController extends ValueNotifier<Message> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
get baseOffset {
|
int get baseOffset => textEditingController.selection.baseOffset;
|
||||||
return textEditingController.selection.baseOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
///
|
||||||
get selectionStart {
|
int get selectionStart => textEditingController.selection.start;
|
||||||
return textEditingController.selection.start;
|
|
||||||
}
|
|
||||||
|
|
||||||
set showInChannel(bool newValue) {
|
set showInChannel(bool newValue) {
|
||||||
value = value.copyWith(showInChannel: newValue);
|
value = value.copyWith(showInChannel: newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
bool get showInChannel {
|
bool get showInChannel => value.showInChannel ?? false;
|
||||||
return value.showInChannel ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
///
|
||||||
List<Attachment> get attachments => value.attachments;
|
List<Attachment> get attachments => value.attachments;
|
||||||
@@ -142,8 +137,9 @@ class MessageInputController extends ValueNotifier<Message> {
|
|||||||
/// After calling this function, [text], [attachments] and [mentionedUsers]
|
/// After calling this function, [text], [attachments] and [mentionedUsers]
|
||||||
/// all will be empty.
|
/// all will be empty.
|
||||||
///
|
///
|
||||||
/// Calling this will notify all the listeners of this [MessageInputController]
|
/// Calling this will notify all the listeners of this
|
||||||
/// that they need to update (it calls [notifyListeners]). For this reason,
|
/// [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
|
/// this method should only be called between frames, e.g. in response to user
|
||||||
/// actions, not during the build, layout, or paint phases.
|
/// actions, not during the build, layout, or paint phases.
|
||||||
void clear() {
|
void clear() {
|
||||||
|
|||||||
Reference in New Issue
Block a user