Merge branch 'develop' into feat/limit-attachment-selection

This commit is contained in:
Salvatore Giordano
2021-08-23 11:14:08 +02:00
63 changed files with 2298 additions and 485 deletions
@@ -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'),
),
),
),
);
}