Merge pull request #626 from GetStream/cds-189
feat(ui, llc): slow mode
This commit is contained in:
@@ -25,6 +25,8 @@ typedef ActionButtonBuilder = Widget Function(
|
||||
> **_NOTE:_** The last parameter is the default `ActionButton`
|
||||
You can call `.copyWith` to customize just a subset of properties.
|
||||
|
||||
- Added slow mode which allows a cooldown period after a user sends a message.
|
||||
|
||||
🔄 Changed
|
||||
|
||||
Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with
|
||||
|
||||
@@ -100,6 +100,9 @@ abstract class Translations {
|
||||
/// The label for write a message in [MessageInput]
|
||||
String get writeAMessageLabel;
|
||||
|
||||
/// The label for slow mode enabled in [MessageInput]
|
||||
String get slowModeOnLabel;
|
||||
|
||||
/// The label for instant commands in [MessageInput]
|
||||
String get instantCommandsLabel;
|
||||
|
||||
@@ -667,4 +670,7 @@ class DefaultTranslations implements Translations {
|
||||
|
||||
@override
|
||||
String get replyToMessageLabel => 'Reply to Message';
|
||||
|
||||
@override
|
||||
String get slowModeOnLabel => 'Slow mode ON';
|
||||
}
|
||||
|
||||
@@ -315,9 +315,15 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
||||
|
||||
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!);
|
||||
@@ -348,6 +354,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(
|
||||
@@ -496,20 +521,25 @@ 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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -781,6 +811,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;
|
||||
}
|
||||
|
||||
@@ -2115,6 +2149,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 +2242,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
_emojiOverlay?.remove();
|
||||
_mentionsOverlay?.remove();
|
||||
_keyboardListener?.cancel();
|
||||
_slowModeTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -2380,3 +2416,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'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,4 +69,69 @@ void main() {
|
||||
expect(find.byKey(const Key('messageInputText')), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'checks message input slow mode',
|
||||
(WidgetTester tester) async {
|
||||
final client = MockClient();
|
||||
final clientState = MockClientState();
|
||||
final channel = MockChannel();
|
||||
final channelState = MockChannelState();
|
||||
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.cooldown).thenReturn(10);
|
||||
when(() => channel.cooldownStartedAt).thenReturn(DateTime.now());
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.isMuted).thenReturn(false);
|
||||
when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false));
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
'name': 'test',
|
||||
}));
|
||||
when(() => channel.extraData).thenReturn({
|
||||
'name': 'test',
|
||||
});
|
||||
when(() => channelState.membersStream).thenAnswer((i) => Stream.value([
|
||||
Member(
|
||||
userId: 'user-id',
|
||||
user: User(id: 'user-id'),
|
||||
)
|
||||
]));
|
||||
when(() => channelState.members).thenReturn([
|
||||
Member(
|
||||
userId: 'user-id',
|
||||
user: User(id: 'user-id'),
|
||||
),
|
||||
]);
|
||||
when(() => channelState.messages).thenReturn([
|
||||
Message(
|
||||
text: 'hello',
|
||||
user: User(id: 'other-user'),
|
||||
)
|
||||
]);
|
||||
when(() => channelState.messagesStream).thenAnswer((i) => Stream.value([
|
||||
Message(
|
||||
text: 'hello',
|
||||
user: User(id: 'other-user'),
|
||||
)
|
||||
]));
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: StreamChat(
|
||||
client: client,
|
||||
child: StreamChannel(
|
||||
channel: channel,
|
||||
child: const Scaffold(
|
||||
body: MessageInput(),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
expect(find.text('Slow mode ON'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user