Skip to content

Commit 57cc503

Browse files
Merge pull request #3143 from acterglobal/kumar/chat-ng-time-view
Chat NG : Chat Time View Enhancement
2 parents acb1c09 + 23d73a2 commit 57cc503

17 files changed

+209
-50
lines changed

.changes/3143-chat-ng-time-view.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- [UI-UX] : Improved chat message area with better alignment of time indicator especially for short messages.

app/lib/common/extensions/acter_build_context.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
33

44
const largeScreenBreakPoint = 770;
55

6+
double defaultMessageMaxWidth(BuildContext context) {
7+
final size = MediaQuery.sizeOf(context);
8+
return context.isLargeScreen ? size.width * 0.5 : size.width * 0.75;
9+
}
10+
611
extension ActerBuildContext on BuildContext {
712
bool get isLargeScreen =>
813
MediaQuery.of(this).size.width >= largeScreenBreakPoint;

app/lib/features/chat_ng/widgets/chat_bubble.dart

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,8 @@ class ChatBubble extends StatelessWidget {
125125
@override
126126
Widget build(BuildContext context) {
127127
final theme = Theme.of(context);
128-
final size = MediaQuery.sizeOf(context);
129128
final msgWidth = messageWidth?.toDouble();
130-
final defaultWidth =
131-
context.isLargeScreen ? size.width * 0.5 : size.width * 0.75;
129+
final defaultWidth = defaultMessageMaxWidth(context);
132130
final String? name = displayName;
133131

134132
final Widget wrappedChild =
@@ -181,8 +179,15 @@ class ChatBubble extends StatelessWidget {
181179
),
182180
const SizedBox(height: 8),
183181
],
184-
wrappedChild,
185-
_buildTimestampAndEditedLabel(context),
182+
Wrap(
183+
clipBehavior: Clip.none,
184+
alignment: WrapAlignment.end,
185+
crossAxisAlignment: WrapCrossAlignment.end,
186+
children: [
187+
wrappedChild,
188+
_buildTimestampAndEditedLabel(context),
189+
],
190+
),
186191
],
187192
),
188193
),
@@ -199,7 +204,9 @@ class ChatBubble extends StatelessWidget {
199204

200205
return Row(
201206
mainAxisAlignment: MainAxisAlignment.end,
207+
mainAxisSize: MainAxisSize.min,
202208
children: [
209+
const SizedBox(width: 6),
203210
if (isEdited) ...[
204211
Text(L10n.of(context).edited, style: textStyle),
205212
const SizedBox(width: 6),

app/lib/features/chat_ng/widgets/events/audio_message_event.dart

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

44
import 'package:acter/common/models/types.dart';
5+
import 'package:acter/common/extensions/acter_build_context.dart';
56
import 'package:acter/common/utils/utils.dart';
67
import 'package:acter/features/chat/models/media_chat_state/media_chat_state.dart';
78
import 'package:acter/features/chat/providers/chat_providers.dart';
@@ -89,31 +90,41 @@ class _AudioMessageEventState extends ConsumerState<AudioMessageEvent> {
8990

9091
Widget _buildAudioEventUI(File? mediaFile, bool isDownloading) {
9192
final msgSize = widget.content.size();
92-
93-
return Row(
94-
children: [
95-
_buildAudioControls(mediaFile, isDownloading),
96-
SizedBox(width: 10),
97-
Expanded(
98-
child: Column(
99-
crossAxisAlignment: CrossAxisAlignment.start,
100-
children: [
101-
Text(widget.content.body()),
102-
Row(
103-
mainAxisAlignment: MainAxisAlignment.spaceBetween,
104-
crossAxisAlignment: CrossAxisAlignment.end,
105-
children: [
106-
if (msgSize != null) Text(formatBytes(msgSize.truncate())),
107-
if (widget.timestamp != null)
108-
MessageTimestampWidget(
109-
timestamp: widget.timestamp.expect('should not be null'),
110-
),
111-
],
112-
),
113-
],
93+
final defaultWidth = defaultMessageMaxWidth(context);
94+
95+
return Container(
96+
constraints: BoxConstraints(maxWidth: defaultWidth),
97+
child: Row(
98+
children: [
99+
_buildAudioControls(mediaFile, isDownloading),
100+
SizedBox(width: 10),
101+
Expanded(
102+
child: Column(
103+
crossAxisAlignment: CrossAxisAlignment.start,
104+
children: [
105+
Text(
106+
widget.content.body(),
107+
maxLines: 2,
108+
overflow: TextOverflow.ellipsis,
109+
),
110+
Row(
111+
crossAxisAlignment: CrossAxisAlignment.end,
112+
children: [
113+
if (msgSize != null) Text(formatBytes(msgSize.truncate())),
114+
Spacer(),
115+
if (widget.timestamp != null)
116+
MessageTimestampWidget(
117+
timestamp: widget.timestamp.expect(
118+
'should not be null',
119+
),
120+
),
121+
],
122+
),
123+
],
124+
),
114125
),
115-
),
116-
],
126+
],
127+
),
117128
);
118129
}
119130

app/lib/features/chat_ng/widgets/events/message_event_item.dart

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,13 @@ class MessageEventItem extends ConsumerWidget {
233233
? RepliedToPreview(roomId: roomId, messageId: messageId, isMe: isMe)
234234
: null;
235235

236-
final child = TextMessageEvent(
237-
content: content,
238-
roomId: roomId,
239-
repliedTo: repliedToBuilder,
240-
isNotice: isNotice,
236+
final child = IntrinsicWidth(
237+
child: TextMessageEvent(
238+
content: content,
239+
roomId: roomId,
240+
repliedTo: repliedToBuilder,
241+
isNotice: isNotice,
242+
),
241243
);
242244

243245
if (isMe) {

app/lib/features/chat_ui_showcase/mocks/showcase/data/general_usecases.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,30 @@ final productTeamMutedWithSingleTypingUserRoom2 = createMockChatItem(
595595
mockBody: 'Will do! See you all tomorrow.',
596596
),
597597
),
598+
MockTimelineEventItem(
599+
mockEventId: 'mock-event-id-49',
600+
mockSenderId: userId,
601+
mockOriginServerTs: 1744096816000, // April 8, 2025 15:40:16
602+
mockMsgContent: MockMsgContent(
603+
mockBody: 'Hy Good morning!',
604+
),
605+
),
606+
MockTimelineEventItem(
607+
mockEventId: 'mock-event-id-50',
608+
mockSenderId: '@michael:acter.global',
609+
mockOriginServerTs: 1744096686000, // April 8, 2025 15:38:06
610+
mockMsgContent: MockMsgContent(
611+
mockBody: 'Hy, good morning! How’s your day starting?',
612+
),
613+
),
614+
MockTimelineEventItem(
615+
mockEventId: 'mock-event-id-51',
616+
mockSenderId: userId,
617+
mockOriginServerTs: 1744096816000, // April 8, 2025 15:40:16
618+
mockMsgContent: MockMsgContent(
619+
mockBody: 'Just wanted to check in and see how everything\'s going on your end. Hope you had a restful night and feel ready to take on the day. Let me know if you need anything or if there\'s something I can help with today!',
620+
),
621+
),
598622

599623
// --- Reply-to-message example ---
600624
MockTimelineEventItem(

app/test/features/chat_ng/chat_room_page/golden_chat_room_page_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
88
import 'package:flutter_localizations/flutter_localizations.dart';
99
import 'package:acter/l10n/generated/l10n.dart';
1010
import 'package:flutter_riverpod/flutter_riverpod.dart';
11+
import 'package:hrk_flutter_test_batteries/hrk_flutter_test_batteries.dart';
1112
import '../../../helpers/font_loader.dart';
1213

1314
void main() {
@@ -23,6 +24,7 @@ void main() {
2324
group('Chat NG - ChatRoomPage golden', () {
2425
testWidgets('ChatRoomPage widget', (tester) async {
2526
await loadTestFonts();
27+
useGoldenFileComparatorWithThreshold(0.06); // 1%
2628

2729
final overrides = [
2830
myUserIdStrProvider.overrideWith((ref) => '@acter1:m-1.acter.global'),
6.14 KB
Loading
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import 'dart:io';
2+
3+
import 'package:acter/common/providers/common_providers.dart';
4+
import 'package:acter/features/chat_ng/widgets/events/chat_event.dart';
5+
import 'package:acter/features/chat_ui_showcase/mocks/showcase/data/general_usecases.dart';
6+
import 'package:acter/features/labs/model/labs_features.dart';
7+
import 'package:acter/features/labs/providers/labs_providers.dart';
8+
import 'package:flutter/material.dart';
9+
import 'package:flutter_test/flutter_test.dart';
10+
import 'package:hrk_flutter_test_batteries/hrk_flutter_test_batteries.dart';
11+
import '../../../../../helpers/font_loader.dart';
12+
import '../../../../../helpers/test_util.dart';
13+
import '../../../../../helpers/utils.dart' as test_utils;
14+
15+
void main() {
16+
final testDir = Directory.current.path;
17+
final goldenDir =
18+
'$testDir/test/features/chat_ng/widgets/chat_room/chat_event_messages/goldens_images';
19+
20+
goldenFileComparator = test_utils.GoldenFileComparator(goldenDir);
21+
group('Chat NG - Chat message time view golden', () {
22+
testWidgets('Chat message time view event widget', (tester) async {
23+
await loadTestFonts();
24+
useGoldenFileComparatorWithThreshold(0.1); // 10%
25+
26+
await tester.pumpProviderWidget(
27+
overrides: [
28+
myUserIdStrProvider.overrideWith((ref) => '@acter1:m-1.acter.global'),
29+
isActiveProvider(LabsFeature.htmlNext).overrideWith((ref) => false),
30+
],
31+
child: ListView(
32+
shrinkWrap: true,
33+
children: [
34+
Material(
35+
child: ChatEvent(
36+
roomId: productTeamMutedWithSingleTypingUserRoom2RoomId,
37+
eventId: 'mock-event-id-49',
38+
),
39+
),
40+
Material(
41+
child: ChatEvent(
42+
roomId: productTeamMutedWithSingleTypingUserRoom2RoomId,
43+
eventId: 'mock-event-id-50',
44+
),
45+
),
46+
Material(
47+
child: ChatEvent(
48+
roomId: productTeamMutedWithSingleTypingUserRoom2RoomId,
49+
eventId: 'mock-event-id-51',
50+
),
51+
),
52+
],
53+
),
54+
);
55+
56+
await tester.pump(const Duration(seconds: 1));
57+
await tester.pump(const Duration(seconds: 1));
58+
await tester.pump(const Duration(seconds: 1));
59+
60+
await expectLater(
61+
find.byType(ListView),
62+
matchesGoldenFile('goldens_images/chat_event_message_time_view.png'),
63+
);
64+
});
65+
});
66+
}

app/test/features/chat_ng/widgets/chat_room/chat_event_messages/golden_chat_event_reactions_message_event_dm_chat_test.dart

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:io';
2+
13
import 'package:acter/common/providers/common_providers.dart';
24
import 'package:acter/features/chat_ng/providers/chat_room_messages_provider.dart';
35
import 'package:acter/features/chat_ng/widgets/events/chat_event.dart';
@@ -9,14 +11,22 @@ import 'package:flutter_test/flutter_test.dart';
911
import 'package:hrk_flutter_test_batteries/hrk_flutter_test_batteries.dart';
1012
import '../../../../../helpers/font_loader.dart';
1113
import '../../../../../helpers/test_util.dart';
14+
import '../../../../../helpers/utils.dart' as test_utils;
1215

1316
void main() {
17+
final testDir = Directory.current.path;
18+
final goldenDir =
19+
'$testDir/test/features/chat_ng/widgets/chat_room/chat_event_messages/goldens_images';
20+
21+
goldenFileComparator = test_utils.GoldenFileComparator(goldenDir);
22+
1423
group('Chat NG : DM Chat - ChatEvent reactions message golden', () {
1524
testWidgets('ChatEvent reactions message event widget - legacy html', (
1625
tester,
1726
) async {
1827
await loadTestFonts();
19-
useGoldenFileComparatorWithThreshold(0.01); // 1%
28+
29+
goldenFileComparator = test_utils.GoldenFileComparator(goldenDir);
2030

2131
await tester.pumpProviderWidget(
2232
overrides: [
@@ -45,9 +55,7 @@ void main() {
4555
),
4656
);
4757

48-
await tester.pump(const Duration(seconds: 1));
49-
await tester.pump(const Duration(seconds: 1));
50-
await tester.pump(const Duration(seconds: 1));
58+
await tester.pumpAndSettle();
5159

5260
await expectLater(
5361
find.byType(ListView),
@@ -60,7 +68,8 @@ void main() {
6068
tester,
6169
) async {
6270
await loadTestFonts();
63-
useGoldenFileComparatorWithThreshold(0.01); // 1%
71+
goldenFileComparator = test_utils.GoldenFileComparator(goldenDir);
72+
useGoldenFileComparatorWithThreshold(0.30);
6473

6574
await tester.pumpProviderWidget(
6675
overrides: [
@@ -89,9 +98,7 @@ void main() {
8998
),
9099
);
91100

92-
await tester.pump(const Duration(seconds: 1));
93-
await tester.pump(const Duration(seconds: 1));
94-
await tester.pump(const Duration(seconds: 1));
101+
await tester.pumpAndSettle();
95102

96103
await expectLater(
97104
find.byType(ListView),

0 commit comments

Comments
 (0)