Skip to content

Commit bcf2c04

Browse files
committed
feat(chat-time-indicator): add small time-indicator in chat detail screens
1 parent 4e0967c commit bcf2c04

File tree

13 files changed

+276
-17
lines changed

13 files changed

+276
-17
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 5.1.0
2+
- Added optional time indicator in chat detail screens to show which day the message is posted
3+
14
## 5.0.0
25
- Removed the default values for the ChatOptions that are now nullable so they resolve to the ThemeData values
36
- Added chatAlignment to change the alignment of the chat messages

packages/chat_repository_interface/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: chat_repository_interface
22
description: "The interface for a chat repository"
3-
version: 5.0.0
3+
version: 5.1.0
44
homepage: "https://github.com/Iconica-Development"
55

66
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub/

packages/firebase_chat_repository/pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: firebase_chat_repository
22
description: "Firebase repository implementation for the chat domain repository interface"
3-
version: 5.0.0
3+
version: 5.1.0
44
homepage: "https://github.com/Iconica-Development"
55

66
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
@@ -15,7 +15,7 @@ dependencies:
1515

1616
chat_repository_interface:
1717
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
18-
version: ^5.0.0
18+
version: ^5.1.0
1919

2020
firebase_storage: any
2121
cloud_firestore: any

packages/flutter_chat/lib/flutter_chat.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export "package:flutter_chat/src/flutter_chat_navigator_userstories.dart";
88
// Options
99
export "src/config/chat_builders.dart";
1010
export "src/config/chat_options.dart";
11+
export "src/config/chat_time_indicator_options.dart";
1112
export "src/config/chat_translations.dart";
1213
export "src/config/screen_types.dart";
1314

packages/flutter_chat/lib/src/config/chat_options.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import "package:cached_network_image/cached_network_image.dart";
2-
import "package:chat_repository_interface/chat_repository_interface.dart";
32
import "package:flutter/material.dart";
4-
import "package:flutter_chat/src/config/chat_builders.dart";
3+
import "package:flutter_chat/flutter_chat.dart";
54
import "package:flutter_chat/src/config/chat_semantics.dart";
6-
import "package:flutter_chat/src/config/chat_translations.dart";
75

86
/// The chat options
97
/// Use this class to configure the chat options.
@@ -28,6 +26,7 @@ class ChatOptions {
2826
this.onNoChats,
2927
this.imageQuality = 20,
3028
this.imageProviderResolver = _defaultImageProviderResolver,
29+
this.timeIndicatorOptions = const ChatTimeIndicatorOptions(),
3130
ChatRepositoryInterface? chatRepository,
3231
UserRepositoryInterface? userRepository,
3332
}) : chatRepository = chatRepository ?? LocalChatRepository(),
@@ -109,6 +108,9 @@ class ChatOptions {
109108
/// the images in the entire userstory. If not provided, CachedNetworkImage
110109
/// will be used.
111110
final ImageProviderResolver imageProviderResolver;
111+
112+
/// Options regarding the time indicator in chat screens
113+
final ChatTimeIndicatorOptions timeIndicatorOptions;
112114
}
113115

114116
/// Typedef for the chatTitleResolver function that is used to get a title for
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import "package:flutter/material.dart";
2+
import "package:flutter_chat/flutter_chat.dart";
3+
import "package:flutter_chat/src/screens/chat_detail/widgets/default_chat_time_indicator.dart";
4+
5+
/// All options related to the time indicator
6+
class ChatTimeIndicatorOptions {
7+
/// Create default ChatTimeIndicator options
8+
const ChatTimeIndicatorOptions({
9+
this.indicatorBuilder = DefaultChatTimeIndicator.builder,
10+
this.labelResolver = defaultChatTimeIndicatorLabelResolver,
11+
this.sectionCheck = defaultChatTimeIndicatorSectionChecker,
12+
});
13+
14+
/// This completely disables the chat time indicator feature
15+
const ChatTimeIndicatorOptions.none()
16+
: indicatorBuilder = DefaultChatTimeIndicator.builder,
17+
labelResolver = defaultChatTimeIndicatorLabelResolver,
18+
sectionCheck = neverShowChatTimeIndicatorSectionChecker;
19+
20+
/// The general builder for the indicator
21+
final ChatTimeIndicatorBuilder indicatorBuilder;
22+
23+
/// A function that translates offset / time to a string label
24+
final ChatTimeIndicatorLabelResolver labelResolver;
25+
26+
/// A function that determines when a new section starts
27+
///
28+
/// By default, all messages are prefixed with a message.
29+
/// You can disable this using the [skipFirstChatTimeIndicatorSectionChecker]
30+
/// instead of the default, which would skip the first section
31+
final ChatTimeIndicatorSectionChecker sectionCheck;
32+
33+
/// public method on the options for readability
34+
bool isMessageInNewTimeSection(
35+
BuildContext context,
36+
MessageModel? previousMessage,
37+
MessageModel currentMessage,
38+
) =>
39+
sectionCheck(
40+
context,
41+
previousMessage,
42+
currentMessage,
43+
);
44+
}
45+
46+
/// A function that would generate a string given the current window/datetime
47+
typedef ChatTimeIndicatorLabelResolver = String Function(
48+
BuildContext context,
49+
int dayOffset,
50+
DateTime currentWindow,
51+
);
52+
53+
/// A function that would determine if a chat indicator has to render
54+
typedef ChatTimeIndicatorSectionChecker = bool Function(
55+
BuildContext context,
56+
MessageModel? previousMessage,
57+
MessageModel currentMessage,
58+
);
59+
60+
/// Build used to render time indicators on chat detail screens
61+
typedef ChatTimeIndicatorBuilder = Widget Function(
62+
BuildContext context,
63+
String timeLabel,
64+
);
65+
66+
///
67+
String defaultChatTimeIndicatorLabelResolver(
68+
BuildContext context,
69+
int dayOffset,
70+
DateTime currentWindow,
71+
) {
72+
var translations = ChatScope.of(context).options.translations;
73+
return translations.chatTimeIndicatorLabel(dayOffset, currentWindow);
74+
}
75+
76+
/// A function that disables the time indicator in chat
77+
bool neverShowChatTimeIndicatorSectionChecker(
78+
BuildContext context,
79+
MessageModel? previousMessage,
80+
MessageModel currentMessage,
81+
) =>
82+
false;
83+
84+
/// Variant of the default implementation for determining if a new section
85+
/// starts, where the first section is skipped.
86+
///
87+
/// Renders a new indicator every new section, skipping the first section
88+
bool skipFirstChatTimeIndicatorSectionChecker(
89+
BuildContext context,
90+
MessageModel? previousMessage,
91+
MessageModel currentMessage,
92+
) =>
93+
previousMessage != null &&
94+
previousMessage.timestamp.date.isBefore(currentMessage.timestamp.date);
95+
96+
/// Default implementation for determining if a new section starts.
97+
///
98+
/// Renders a new indicator every new section
99+
bool defaultChatTimeIndicatorSectionChecker(
100+
BuildContext context,
101+
MessageModel? previousMessage,
102+
MessageModel currentMessage,
103+
) =>
104+
previousMessage == null ||
105+
previousMessage.timestamp.date.isBefore(currentMessage.timestamp.date);

packages/flutter_chat/lib/src/config/chat_translations.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
// ignore_for_file: public_member_api_docs
66

7+
import "package:intl/intl.dart";
8+
79
/// Class that holds all the translations for the chat component view and
810
/// the corresponding userstory
911
class ChatTranslations {
@@ -50,6 +52,7 @@ class ChatTranslations {
5052
required this.groupNameEmpty,
5153
required this.messagesLoadingError,
5254
required this.next,
55+
required this.chatTimeIndicatorLabel,
5356
});
5457

5558
/// Default translations for the chat component view
@@ -95,6 +98,8 @@ class ChatTranslations {
9598
this.groupNameEmpty = "Group",
9699
this.messagesLoadingError = "Error loading messages, you can reload below:",
97100
this.next = "Next",
101+
this.chatTimeIndicatorLabel =
102+
ChatTranslations.defaultChatTimeIndicatorLabel,
98103
});
99104

100105
final String chatsTitle;
@@ -140,6 +145,33 @@ class ChatTranslations {
140145
/// to be loaded.
141146
final String messagesLoadingError;
142147

148+
/// The message of a label given a certain offset.
149+
///
150+
/// The offset determines whether it is today (0), yesterday (-1), or earlier.
151+
///
152+
/// [dateOffset] will rarely be a +1, however if anyone ever wants to see
153+
/// future chat messages, then this number will be positive.
154+
///
155+
/// use the given [time] format to display exact time information.
156+
final String Function(int dateOffset, DateTime time) chatTimeIndicatorLabel;
157+
158+
/// Standard function to convert an offset to a String.
159+
///
160+
/// Recommended to always override this in any production app with an
161+
/// app localizations implementation.
162+
static String defaultChatTimeIndicatorLabel(
163+
int dateOffset,
164+
DateTime time,
165+
) =>
166+
switch (dateOffset) {
167+
0 => "Today",
168+
-1 => "Yesterday",
169+
1 => "Tomorrow",
170+
int value when value < 5 && value > 1 => "In $value days",
171+
int value when value < -1 && value > -5 => "$value days ago",
172+
_ => DateFormat("dd-MM-YYYY").format(time),
173+
};
174+
143175
final String next;
144176

145177
// copyWith method to override the default values
@@ -182,6 +214,7 @@ class ChatTranslations {
182214
String? groupNameEmpty,
183215
String? messagesLoadingError,
184216
String? next,
217+
String Function(int dateOffset, DateTime time)? chatTimeIndicatorLabel,
185218
}) =>
186219
ChatTranslations(
187220
chatsTitle: chatsTitle ?? this.chatsTitle,
@@ -234,5 +267,7 @@ class ChatTranslations {
234267
groupNameEmpty: groupNameEmpty ?? this.groupNameEmpty,
235268
messagesLoadingError: messagesLoadingError ?? this.messagesLoadingError,
236269
next: next ?? this.next,
270+
chatTimeIndicatorLabel:
271+
chatTimeIndicatorLabel ?? this.chatTimeIndicatorLabel,
237272
);
238273
}

packages/flutter_chat/lib/src/screens/chat_detail/chat_detail_screen.dart

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import "dart:async";
22
import "dart:typed_data";
33

4-
import "package:chat_repository_interface/chat_repository_interface.dart";
54
import "package:flutter/material.dart";
65
import "package:flutter_accessibility/flutter_accessibility.dart";
7-
import "package:flutter_chat/src/config/chat_options.dart";
8-
import "package:flutter_chat/src/config/screen_types.dart";
6+
import "package:flutter_chat/flutter_chat.dart";
97
import "package:flutter_chat/src/screens/chat_detail/widgets/chat_bottom.dart";
108
import "package:flutter_chat/src/screens/chat_detail/widgets/chat_widgets.dart";
119
import "package:flutter_chat/src/screens/creation/widgets/default_image_picker.dart";
12-
import "package:flutter_chat/src/util/scope.dart";
1310
import "package:flutter_hooks/flutter_hooks.dart";
1411

1512
/// Chat detail screen
@@ -465,14 +462,26 @@ class _ChatBody extends HookWidget {
465462
bubbleChildren
466463
.add(ChatNoMessages(isGroupChat: chat?.isGroupChat ?? false));
467464
} else {
468-
for (var (index, msg) in messages.indexed) {
469-
var prevMsg = index > 0 ? messages[index - 1] : null;
465+
for (var (index, currentMessage) in messages.indexed) {
466+
var previousMessage = index > 0 ? messages[index - 1] : null;
467+
468+
if (options.timeIndicatorOptions.isMessageInNewTimeSection(
469+
context,
470+
previousMessage,
471+
currentMessage,
472+
)) {
473+
bubbleChildren.add(
474+
ChatTimeIndicator(
475+
forDate: currentMessage.timestamp,
476+
),
477+
);
478+
}
470479

471480
bubbleChildren.add(
472481
ChatBubble(
473-
message: msg,
474-
previousMessage: prevMsg,
475-
sender: userMap[msg.senderId],
482+
message: currentMessage,
483+
previousMessage: previousMessage,
484+
sender: userMap[currentMessage.senderId],
476485
onPressSender: onPressUserProfile,
477486
semanticIdTitle: options.semantics.chatBubbleTitle(index),
478487
semanticIdTime: options.semantics.chatBubbleTime(index),

packages/flutter_chat/lib/src/screens/chat_detail/widgets/chat_widgets.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import "package:flutter/material.dart";
33
import "package:flutter_accessibility/flutter_accessibility.dart";
44
import "package:flutter_chat/src/screens/chat_detail/widgets/default_message_builder.dart";
55
import "package:flutter_chat/src/util/scope.dart";
6+
import "package:flutter_chat/src/util/utils.dart";
67
import "package:flutter_hooks/flutter_hooks.dart";
78

89
/// Widget displayed when there are no messages in the chat.
@@ -102,3 +103,32 @@ class ChatBubble extends HookWidget {
102103
);
103104
}
104105
}
106+
107+
/// The indicator above a set of messages, shown per date.
108+
class ChatTimeIndicator extends StatelessWidget {
109+
/// Creates a ChatTimeIndicator
110+
const ChatTimeIndicator({
111+
required this.forDate,
112+
super.key,
113+
});
114+
115+
/// The dateTime at which the new time section starts
116+
final DateTime forDate;
117+
118+
@override
119+
Widget build(BuildContext context) {
120+
var scope = ChatScope.of(context);
121+
var indicatorOptions = scope.options.timeIndicatorOptions;
122+
123+
var today = DateTime.now();
124+
var differenceInDays = today.getDateOffsetInDays(forDate);
125+
126+
var message = indicatorOptions.labelResolver(
127+
context,
128+
differenceInDays,
129+
forDate,
130+
);
131+
132+
return indicatorOptions.indicatorBuilder(context, message);
133+
}
134+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import "package:flutter/material.dart";
2+
import "package:flutter_chat/flutter_chat.dart";
3+
4+
/// The default layout for a chat indicator
5+
class DefaultChatTimeIndicator extends StatelessWidget {
6+
/// Create a default timeindicator in a chat
7+
const DefaultChatTimeIndicator({
8+
required this.timeIndicatorString,
9+
super.key,
10+
});
11+
12+
/// The text shown in the time indicator
13+
final String timeIndicatorString;
14+
15+
/// Standard builder for time indication
16+
static Widget builder(BuildContext context, String timeIndicatorString) =>
17+
DefaultChatTimeIndicator(timeIndicatorString: timeIndicatorString);
18+
19+
@override
20+
Widget build(BuildContext context) {
21+
var theme = Theme.of(context);
22+
var spacing = ChatScope.of(context).options.spacing;
23+
return Center(
24+
child: Container(
25+
margin: EdgeInsets.only(top: spacing.chatBetweenMessagesPadding),
26+
padding: const EdgeInsets.symmetric(
27+
vertical: 4,
28+
horizontal: 8,
29+
),
30+
decoration: BoxDecoration(
31+
borderRadius: BorderRadius.circular(6),
32+
color: theme.colorScheme.surfaceContainerHighest,
33+
),
34+
child: Text(
35+
timeIndicatorString,
36+
style: theme.textTheme.labelSmall?.copyWith(
37+
color: theme.colorScheme.onSurfaceVariant,
38+
),
39+
),
40+
),
41+
);
42+
}
43+
}

0 commit comments

Comments
 (0)