Merge branch 'develop' into feat/limit-attachment-selection
This commit is contained in:
@@ -60,6 +60,15 @@ typedef MentionTileBuilder = Widget Function(
|
||||
Member member,
|
||||
);
|
||||
|
||||
/// Widget builder for action button.
|
||||
///
|
||||
/// [defaultActionButton] is the default [IconButton] configuration,
|
||||
/// use [defaultActionButton.copyWith] to easily customize it.
|
||||
typedef ActionButtonBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
IconButton defaultActionButton,
|
||||
);
|
||||
|
||||
/// Location for actions on the [MessageInput]
|
||||
enum ActionsLocation {
|
||||
/// Align to left
|
||||
@@ -176,6 +185,8 @@ class MessageInput extends StatefulWidget {
|
||||
this.onError,
|
||||
this.attachmentLimit = 10,
|
||||
this.onAttachmentLimitExceed,
|
||||
this.attachmentButtonBuilder,
|
||||
this.commandButtonBuilder,
|
||||
}) : assert(
|
||||
initialMessage == null || editMessage == null,
|
||||
"Can't provide both `initialMessage` and `editMessage`",
|
||||
@@ -271,6 +282,18 @@ class MessageInput extends StatefulWidget {
|
||||
/// This will override the default error alert behaviour.
|
||||
final AttachmentLimitExceedListener? onAttachmentLimitExceed;
|
||||
|
||||
/// Builder for customizing the attachment button.
|
||||
///
|
||||
/// The builder contains the default [IconButton] that can be customized by
|
||||
/// calling `.copyWith`.
|
||||
final ActionButtonBuilder? attachmentButtonBuilder;
|
||||
|
||||
/// Builder for customizing the command button.
|
||||
///
|
||||
/// The builder contains the default [IconButton] that can be customized by
|
||||
/// calling `.copyWith`.
|
||||
final ActionButtonBuilder? commandButtonBuilder;
|
||||
|
||||
@override
|
||||
MessageInputState createState() => MessageInputState();
|
||||
|
||||
@@ -316,10 +339,15 @@ class MessageInputState extends State<MessageInput> {
|
||||
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
||||
|
||||
bool get _messageIsPresent => _textEditingController.text.trim().isNotEmpty;
|
||||
late DateTime? _cooldownStartedAt;
|
||||
int? _timeOut;
|
||||
|
||||
Timer? _slowModeTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startSlowMode();
|
||||
_focusNode = widget.focusNode ?? FocusNode();
|
||||
_emojiNames =
|
||||
Emoji.all().where((it) => it.name != null).map((e) => e.name!);
|
||||
@@ -350,6 +378,25 @@ class MessageInputState extends State<MessageInput> {
|
||||
});
|
||||
}
|
||||
|
||||
void _startSlowMode() {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
if (channel.cooldownStartedAt != null) {
|
||||
_cooldownStartedAt = channel.cooldownStartedAt;
|
||||
if (DateTime.now().difference(_cooldownStartedAt!).inSeconds <
|
||||
channel.cooldown!) {
|
||||
_timeOut = channel.cooldown! -
|
||||
DateTime.now().difference(_cooldownStartedAt!).inSeconds;
|
||||
_slowModeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (_timeOut == 0) {
|
||||
timer.cancel();
|
||||
} else {
|
||||
setState(() => _timeOut = _timeOut! - 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = DecoratedBox(
|
||||
@@ -428,11 +475,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
children: <Widget>[
|
||||
if (!_commandEnabled &&
|
||||
widget.actionsLocation == ActionsLocation.left)
|
||||
_buildExpandActionsButton(),
|
||||
_buildExpandActionsButton(context),
|
||||
_buildTextInput(context),
|
||||
if (!_commandEnabled &&
|
||||
widget.actionsLocation == ActionsLocation.right)
|
||||
_buildExpandActionsButton(),
|
||||
_buildExpandActionsButton(context),
|
||||
if (widget.sendButtonLocation == SendButtonLocation.outside)
|
||||
_animateSendButton(context),
|
||||
],
|
||||
@@ -498,24 +545,29 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
|
||||
Widget _animateSendButton(BuildContext context) {
|
||||
final sendButton = widget.activeSendButton != null
|
||||
? InkWell(
|
||||
onTap: sendMessage,
|
||||
child: widget.activeSendButton,
|
||||
)
|
||||
: _buildSendButton(context);
|
||||
return AnimatedCrossFade(
|
||||
crossFadeState: (_messageIsPresent || _attachments.isNotEmpty)
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: sendButton,
|
||||
secondChild: widget.idleSendButton ?? _buildIdleSendButton(context),
|
||||
duration: _messageInputTheme.sendAnimationDuration!,
|
||||
alignment: Alignment.center,
|
||||
late Widget sendButton;
|
||||
if (_timeOut != null && _timeOut! > 0) {
|
||||
sendButton = _CountdownButton(
|
||||
count: _timeOut!,
|
||||
);
|
||||
} else if (!_messageIsPresent && _attachments.isEmpty) {
|
||||
sendButton = widget.idleSendButton ?? _buildIdleSendButton(context);
|
||||
} else {
|
||||
sendButton = widget.activeSendButton != null
|
||||
? InkWell(
|
||||
onTap: sendMessage,
|
||||
child: widget.activeSendButton,
|
||||
)
|
||||
: _buildSendButton(context);
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!,
|
||||
child: sendButton,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExpandActionsButton() {
|
||||
Widget _buildExpandActionsButton(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
@@ -553,12 +605,13 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
if (!widget.disableAttachments) _buildAttachmentButton(),
|
||||
if (!widget.disableAttachments)
|
||||
_buildAttachmentButton(context),
|
||||
if (widget.showCommandsButton &&
|
||||
widget.editMessage == null &&
|
||||
channel.state != null &&
|
||||
channel.config?.commands.isNotEmpty == true)
|
||||
_buildCommandButton(),
|
||||
_buildCommandButton(context),
|
||||
...widget.actions ?? [],
|
||||
].insertBetween(const SizedBox(width: 8)),
|
||||
),
|
||||
@@ -694,9 +747,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
: (widget.actionsLocation == ActionsLocation.leftInside
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildExpandActionsButton(),
|
||||
],
|
||||
children: [_buildExpandActionsButton(context)],
|
||||
)
|
||||
: null),
|
||||
suffixIconConstraints: const BoxConstraints.tightFor(height: 40),
|
||||
@@ -722,7 +773,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
),
|
||||
if (!_commandEnabled &&
|
||||
widget.actionsLocation == ActionsLocation.rightInside)
|
||||
_buildExpandActionsButton(),
|
||||
_buildExpandActionsButton(context),
|
||||
if (widget.sendButtonLocation == SendButtonLocation.inside)
|
||||
_animateSendButton(context),
|
||||
],
|
||||
@@ -783,6 +834,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
if (_attachments.isNotEmpty) {
|
||||
return context.translations.addACommentOrSendLabel;
|
||||
}
|
||||
if (_timeOut != 0 && _timeOut != null) {
|
||||
return context.translations.slowModeOnLabel;
|
||||
}
|
||||
|
||||
return context.translations.writeAMessageLabel;
|
||||
}
|
||||
|
||||
@@ -1702,10 +1757,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildCommandButton() {
|
||||
Widget _buildCommandButton(BuildContext context) {
|
||||
final s = _textEditingController.text.trim();
|
||||
|
||||
return IconButton(
|
||||
final defaultButton = IconButton(
|
||||
icon: StreamSvgIcon.lightning(
|
||||
color: s.isNotEmpty
|
||||
? _streamChatTheme.colorTheme.disabled
|
||||
@@ -1743,38 +1797,46 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return widget.commandButtonBuilder?.call(context, defaultButton) ??
|
||||
defaultButton;
|
||||
}
|
||||
|
||||
Widget _buildAttachmentButton() => IconButton(
|
||||
icon: StreamSvgIcon.attach(
|
||||
color: _openFilePickerSection
|
||||
? _messageInputTheme.actionButtonColor
|
||||
: _messageInputTheme.actionButtonIdleColor,
|
||||
),
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
splashRadius: 24,
|
||||
onPressed: () async {
|
||||
_emojiOverlay?.remove();
|
||||
_emojiOverlay = null;
|
||||
_commandsOverlay?.remove();
|
||||
_commandsOverlay = null;
|
||||
_mentionsOverlay?.remove();
|
||||
_mentionsOverlay = null;
|
||||
Widget _buildAttachmentButton(BuildContext context) {
|
||||
final defaultButton = IconButton(
|
||||
icon: StreamSvgIcon.attach(
|
||||
color: _openFilePickerSection
|
||||
? _messageInputTheme.actionButtonColor
|
||||
: _messageInputTheme.actionButtonIdleColor,
|
||||
),
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
splashRadius: 24,
|
||||
onPressed: () async {
|
||||
_emojiOverlay?.remove();
|
||||
_emojiOverlay = null;
|
||||
_commandsOverlay?.remove();
|
||||
_commandsOverlay = null;
|
||||
_mentionsOverlay?.remove();
|
||||
_mentionsOverlay = null;
|
||||
|
||||
if (_openFilePickerSection) {
|
||||
setState(() {
|
||||
_openFilePickerSection = false;
|
||||
_filePickerSize = _kMinMediaPickerSize;
|
||||
});
|
||||
} else {
|
||||
showAttachmentModal();
|
||||
}
|
||||
},
|
||||
);
|
||||
if (_openFilePickerSection) {
|
||||
setState(() {
|
||||
_openFilePickerSection = false;
|
||||
_filePickerSize = _kMinMediaPickerSize;
|
||||
});
|
||||
} else {
|
||||
showAttachmentModal();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return widget.attachmentButtonBuilder?.call(context, defaultButton) ??
|
||||
defaultButton;
|
||||
}
|
||||
|
||||
/// Show the attachment modal, making the user choose where to
|
||||
/// pick a media from
|
||||
@@ -1904,15 +1966,14 @@ class MessageInputState extends State<MessageInput> {
|
||||
} else if (fileType == DefaultAttachmentTypes.video) {
|
||||
pickedFile = await _imagePicker.pickVideo(source: ImageSource.camera);
|
||||
}
|
||||
if (pickedFile == null) {
|
||||
return;
|
||||
if (pickedFile != null) {
|
||||
final bytes = await pickedFile.readAsBytes();
|
||||
file = AttachmentFile(
|
||||
size: bytes.length,
|
||||
path: pickedFile.path,
|
||||
bytes: bytes,
|
||||
);
|
||||
}
|
||||
final bytes = await pickedFile.readAsBytes();
|
||||
file = AttachmentFile(
|
||||
size: bytes.length,
|
||||
path: pickedFile.path,
|
||||
bytes: bytes,
|
||||
);
|
||||
} else {
|
||||
late FileType type;
|
||||
if (fileType == DefaultAttachmentTypes.image) {
|
||||
@@ -2119,6 +2180,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
if (resp.message?.type == 'error') {
|
||||
_parseExistingMessage(message);
|
||||
}
|
||||
_startSlowMode();
|
||||
widget.onMessageSent?.call(resp.message);
|
||||
} catch (e, stk) {
|
||||
if (widget.onError != null) {
|
||||
@@ -2207,6 +2269,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
_emojiOverlay?.remove();
|
||||
_mentionsOverlay?.remove();
|
||||
_keyboardListener?.cancel();
|
||||
_slowModeTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -2332,3 +2395,30 @@ 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'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user