Skip to content

Commit bb02a31

Browse files
TW-2635 Add typing timer to receiver
1 parent 04076f4 commit bb02a31

File tree

5 files changed

+196
-45
lines changed

5 files changed

+196
-45
lines changed

lib/pages/chat/chat_app_bar_title.dart

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:async';
22

33
import 'package:connectivity_plus/connectivity_plus.dart';
44
import 'package:fluffychat/pages/chat/chat_app_bar_title_style.dart';
5+
import 'package:fluffychat/pages/chat/typing_timer_wrapper.dart';
56
import 'package:fluffychat/resource/image_paths.dart';
67
import 'package:fluffychat/utils/common_helper.dart';
78
import 'package:fluffychat/utils/room_status_extension.dart';
@@ -203,19 +204,21 @@ class _DirectChatAppBarStatusContent extends StatelessWidget {
203204
text: L10n.of(context)!.loadingStatus,
204205
);
205206
}
206-
final typingText = room.getLocalizedTypingText(context);
207-
if (typingText.isEmpty) {
208-
return ChatAppBarTitleText(
207+
return TypingTimerWrapper(
208+
room: room,
209+
l10n: L10n.of(context)!,
210+
typingWidget: _ChatAppBarTitleTyping(
211+
typingText: room.getLocalizedTypingText(L10n.of(context)!),
212+
),
213+
notTypingWidget: ChatAppBarTitleText(
209214
text: room
210215
.getLocalizedStatus(
211216
context,
212217
presence: directChatPresence,
213218
)
214219
.capitalize(context),
215-
);
216-
} else {
217-
return _ChatAppBarTitleTyping(typingText: typingText);
218-
}
220+
),
221+
);
219222
},
220223
);
221224
},
@@ -247,14 +250,16 @@ class _GroupChatAppBarStatusContent extends StatelessWidget {
247250
if (snapshot.hasData && connectivityResult == ConnectivityResult.none) {
248251
return ChatAppBarTitleText(text: L10n.of(context)!.noConnection);
249252
}
250-
final typingText = room.getLocalizedTypingText(context);
251-
if (typingText.isEmpty) {
252-
return ChatAppBarTitleText(
253+
return TypingTimerWrapper(
254+
room: room,
255+
l10n: L10n.of(context)!,
256+
typingWidget: _ChatAppBarTitleTyping(
257+
typingText: room.getLocalizedTypingText(L10n.of(context)!),
258+
),
259+
notTypingWidget: ChatAppBarTitleText(
253260
text: room.getLocalizedStatus(context).capitalize(context),
254-
);
255-
} else {
256-
return _ChatAppBarTitleTyping(typingText: typingText);
257-
}
261+
),
262+
);
258263
},
259264
);
260265
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import 'package:async/async.dart';
2+
import 'package:fluffychat/generated/l10n/app_localizations.dart';
3+
import 'package:fluffychat/utils/room_status_extension.dart';
4+
import 'package:flutter/widgets.dart';
5+
import 'package:matrix/matrix.dart';
6+
7+
class TypingTimerWrapper extends StatefulWidget {
8+
const TypingTimerWrapper({
9+
super.key,
10+
required this.room,
11+
required this.l10n,
12+
required this.typingWidget,
13+
this.notTypingWidget = const SizedBox(),
14+
});
15+
16+
final Room room;
17+
final L10n l10n;
18+
final Widget typingWidget;
19+
final Widget notTypingWidget;
20+
21+
@override
22+
State<TypingTimerWrapper> createState() => _TypingTimerWrapperState();
23+
}
24+
25+
class _TypingTimerWrapperState extends State<TypingTimerWrapper> {
26+
bool showTyping = false;
27+
late RestartableTimer timer;
28+
29+
void checkTyping() {
30+
if (!mounted) return;
31+
32+
if (widget.room.typingUsers.isEmpty) {
33+
showTyping = false;
34+
timer.cancel();
35+
return;
36+
}
37+
38+
if (widget.room.getLocalizedTypingText(widget.l10n).isNotEmpty) {
39+
showTyping = true;
40+
timer.reset();
41+
}
42+
}
43+
44+
@override
45+
void initState() {
46+
super.initState();
47+
timer = RestartableTimer(
48+
const Duration(seconds: 30),
49+
() {
50+
if (!mounted) return;
51+
52+
setState(() {
53+
showTyping = false;
54+
});
55+
},
56+
)..cancel();
57+
checkTyping();
58+
}
59+
60+
@override
61+
void didUpdateWidget(covariant TypingTimerWrapper oldWidget) {
62+
super.didUpdateWidget(oldWidget);
63+
checkTyping();
64+
}
65+
66+
@override
67+
void dispose() {
68+
timer.cancel();
69+
super.dispose();
70+
}
71+
72+
@override
73+
Widget build(BuildContext context) {
74+
if (showTyping) return widget.typingWidget;
75+
76+
return widget.notTypingWidget;
77+
}
78+
}

lib/pages/chat_list/chat_list_item_subtitle.dart

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:fluffychat/config/app_config.dart';
22
import 'package:fluffychat/config/themes.dart';
33
import 'package:fluffychat/domain/model/room/room_extension.dart';
4+
import 'package:fluffychat/pages/chat/typing_timer_wrapper.dart';
45
import 'package:fluffychat/presentation/mixins/chat_list_item_mixin.dart';
56
import 'package:fluffychat/pages/chat_list/chat_list_item_style.dart';
67
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
@@ -20,7 +21,7 @@ class ChatListItemSubtitle extends StatelessWidget with ChatListItemMixin {
2021

2122
@override
2223
Widget build(BuildContext context) {
23-
final typingText = room.getLocalizedTypingText(context);
24+
final typingText = room.getLocalizedTypingText(L10n.of(context)!);
2425
final isGroup = !room.isDirectChat;
2526
final unreadBadgeSize = ChatListItemStyle.unreadBadgeSize(
2627
room.isUnreadOrInvited,
@@ -166,25 +167,28 @@ class ChatListItemSubtitle extends StatelessWidget with ChatListItemMixin {
166167
bool isGroup,
167168
bool isMediaEvent,
168169
) {
169-
return typingText.isNotEmpty
170-
? typingTextWidget(typingText, context)
171-
: isGroup
172-
? chatListItemSubtitleForGroup(
173-
context: context,
174-
room: room,
175-
event: lastEvent,
176-
)
177-
: isMediaEvent
178-
? chatListItemMediaPreviewSubTitle(
179-
context,
180-
lastEvent,
181-
)
182-
: textContentWidget(
183-
room,
184-
lastEvent,
185-
context,
186-
isGroup,
187-
room.isUnreadOrInvited,
188-
);
170+
return TypingTimerWrapper(
171+
room: room,
172+
l10n: L10n.of(context)!,
173+
typingWidget: typingTextWidget(typingText, context),
174+
notTypingWidget: isGroup
175+
? chatListItemSubtitleForGroup(
176+
context: context,
177+
room: room,
178+
event: lastEvent,
179+
)
180+
: isMediaEvent
181+
? chatListItemMediaPreviewSubTitle(
182+
context,
183+
lastEvent,
184+
)
185+
: textContentWidget(
186+
room,
187+
lastEvent,
188+
context,
189+
isGroup,
190+
room.isUnreadOrInvited,
191+
),
192+
);
189193
}
190194
}

lib/utils/room_status_extension.dart

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,30 +22,30 @@ extension RoomStatusExtension on Room {
2222
return _getLocalizedStatusGroupChat(context);
2323
}
2424

25-
String getLocalizedTypingText(BuildContext context) {
25+
String getLocalizedTypingText(L10n l10n) {
2626
var typingText = '';
2727
final typingUsers = this.typingUsers;
2828
typingUsers.removeWhere((User u) => u.id == client.userID);
2929

30+
if (typingUsers.isEmpty) return '';
31+
3032
if (AppConfig.hideTypingUsernames) {
31-
typingText = L10n.of(context)!.isTyping;
33+
typingText = l10n.isTyping;
3234
if (typingUsers.first.id != directChatMatrixID) {
33-
typingText =
34-
L10n.of(context)!.numUsersTyping(typingUsers.length.toString());
35+
typingText = l10n.numUsersTyping(typingUsers.length.toString());
3536
}
3637
} else if (typingUsers.length == 1) {
37-
typingText = L10n.of(context)!.isTyping;
38+
typingText = l10n.isTyping;
3839
if (typingUsers.first.id != directChatMatrixID) {
39-
typingText =
40-
L10n.of(context)!.userIsTyping(typingUsers.first.calcDisplayname());
40+
typingText = l10n.userIsTyping(typingUsers.first.calcDisplayname());
4141
}
4242
} else if (typingUsers.length == 2) {
43-
typingText = L10n.of(context)!.userAndUserAreTyping(
43+
typingText = l10n.userAndUserAreTyping(
4444
typingUsers.first.calcDisplayname(),
4545
typingUsers[1].calcDisplayname(),
4646
);
4747
} else if (typingUsers.length > 2) {
48-
typingText = L10n.of(context)!.userAndOthersAreTyping(
48+
typingText = l10n.userAndOthersAreTyping(
4949
typingUsers.first.calcDisplayname(),
5050
(typingUsers.length - 1).toString(),
5151
);
@@ -86,7 +86,7 @@ extension RoomStatusExtension on Room {
8686
}
8787

8888
bool isTypingText(BuildContext context) {
89-
return getLocalizedTypingText(context).isNotEmpty &&
89+
return getLocalizedTypingText(L10n.of(context)!).isNotEmpty &&
9090
lastEvent?.senderId == client.userID &&
9191
lastEvent!.status.isSending;
9292
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import 'package:fluffychat/generated/l10n/app_localizations.dart';
2+
import 'package:fluffychat/pages/chat/typing_timer_wrapper.dart';
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
import 'package:matrix/matrix.dart';
6+
import 'package:mockito/annotations.dart';
7+
import 'package:mockito/mockito.dart';
8+
9+
import 'typing_timer_wrapper_test.mocks.dart';
10+
11+
@GenerateNiceMocks([
12+
MockSpec<Room>(),
13+
MockSpec<Client>(),
14+
MockSpec<L10n>(),
15+
])
16+
void main() {
17+
testWidgets('typing timer wrapper should stop showing typing after 30s',
18+
(tester) async {
19+
final room = MockRoom();
20+
final client = MockClient();
21+
final l10n = MockL10n();
22+
const typingText = 'typing';
23+
const notTypingText = 'not typing';
24+
const typingWidget = Text(typingText);
25+
const notTypingWidget = Text(notTypingText);
26+
when(client.userID).thenReturn('owner');
27+
when(room.client).thenReturn(client);
28+
when(room.typingUsers).thenReturn([]);
29+
await tester.pumpWidget(
30+
MaterialApp(
31+
home: Scaffold(
32+
body: TypingTimerWrapper(
33+
room: room,
34+
l10n: l10n,
35+
typingWidget: typingWidget,
36+
notTypingWidget: notTypingWidget,
37+
),
38+
),
39+
),
40+
);
41+
expect(find.text(notTypingText), findsOneWidget);
42+
expect(find.text(typingText), findsNothing);
43+
44+
when(room.typingUsers).thenReturn([User('id', room: room)]);
45+
await tester.pumpWidget(
46+
MaterialApp(
47+
home: Scaffold(
48+
body: TypingTimerWrapper(
49+
room: room,
50+
l10n: l10n,
51+
typingWidget: typingWidget,
52+
notTypingWidget: notTypingWidget,
53+
),
54+
),
55+
),
56+
);
57+
expect(find.text(typingText), findsOneWidget);
58+
expect(find.text(notTypingText), findsNothing);
59+
60+
await tester.pump(const Duration(seconds: 30));
61+
expect(find.text(notTypingText), findsOneWidget);
62+
expect(find.text(typingText), findsNothing);
63+
});
64+
}

0 commit comments

Comments
 (0)