mirror of
https://github.com/Iconica-Development/flutter_chat.git
synced 2025-05-18 18:33:49 +02:00
feat(chat-time-indicator): add small time-indicator in chat detail screens
This commit is contained in:
parent
4e0967cc33
commit
a0298eb318
13 changed files with 276 additions and 17 deletions
|
@ -1,3 +1,6 @@
|
|||
## 5.1.0
|
||||
- Added optional time indicator in chat detail screens to show which day the message is posted
|
||||
|
||||
## 5.0.0
|
||||
- Removed the default values for the ChatOptions that are now nullable so they resolve to the ThemeData values
|
||||
- Added chatAlignment to change the alignment of the chat messages
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: chat_repository_interface
|
||||
description: "The interface for a chat repository"
|
||||
version: 5.0.0
|
||||
version: 5.1.0
|
||||
homepage: "https://github.com/Iconica-Development"
|
||||
|
||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: firebase_chat_repository
|
||||
description: "Firebase repository implementation for the chat domain repository interface"
|
||||
version: 5.0.0
|
||||
version: 5.1.0
|
||||
homepage: "https://github.com/Iconica-Development"
|
||||
|
||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
|
||||
|
@ -15,7 +15,7 @@ dependencies:
|
|||
|
||||
chat_repository_interface:
|
||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
version: ^5.0.0
|
||||
version: ^5.1.0
|
||||
|
||||
firebase_storage: any
|
||||
cloud_firestore: any
|
||||
|
|
|
@ -8,6 +8,7 @@ export "package:flutter_chat/src/flutter_chat_navigator_userstories.dart";
|
|||
// Options
|
||||
export "src/config/chat_builders.dart";
|
||||
export "src/config/chat_options.dart";
|
||||
export "src/config/chat_time_indicator_options.dart";
|
||||
export "src/config/chat_translations.dart";
|
||||
export "src/config/screen_types.dart";
|
||||
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import "package:cached_network_image/cached_network_image.dart";
|
||||
import "package:chat_repository_interface/chat_repository_interface.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_chat/src/config/chat_builders.dart";
|
||||
import "package:flutter_chat/flutter_chat.dart";
|
||||
import "package:flutter_chat/src/config/chat_semantics.dart";
|
||||
import "package:flutter_chat/src/config/chat_translations.dart";
|
||||
|
||||
/// The chat options
|
||||
/// Use this class to configure the chat options.
|
||||
|
@ -28,6 +26,7 @@ class ChatOptions {
|
|||
this.onNoChats,
|
||||
this.imageQuality = 20,
|
||||
this.imageProviderResolver = _defaultImageProviderResolver,
|
||||
this.timeIndicatorOptions = const ChatTimeIndicatorOptions(),
|
||||
ChatRepositoryInterface? chatRepository,
|
||||
UserRepositoryInterface? userRepository,
|
||||
}) : chatRepository = chatRepository ?? LocalChatRepository(),
|
||||
|
@ -109,6 +108,9 @@ class ChatOptions {
|
|||
/// the images in the entire userstory. If not provided, CachedNetworkImage
|
||||
/// will be used.
|
||||
final ImageProviderResolver imageProviderResolver;
|
||||
|
||||
/// Options regarding the time indicator in chat screens
|
||||
final ChatTimeIndicatorOptions timeIndicatorOptions;
|
||||
}
|
||||
|
||||
/// Typedef for the chatTitleResolver function that is used to get a title for
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_chat/flutter_chat.dart";
|
||||
import "package:flutter_chat/src/screens/chat_detail/widgets/default_chat_time_indicator.dart";
|
||||
|
||||
/// All options related to the time indicator
|
||||
class ChatTimeIndicatorOptions {
|
||||
/// Create default ChatTimeIndicator options
|
||||
const ChatTimeIndicatorOptions({
|
||||
this.indicatorBuilder = DefaultChatTimeIndicator.builder,
|
||||
this.labelResolver = defaultChatTimeIndicatorLabelResolver,
|
||||
this.sectionCheck = defaultChatTimeIndicatorSectionChecker,
|
||||
});
|
||||
|
||||
/// This completely disables the chat time indicator feature
|
||||
const ChatTimeIndicatorOptions.none()
|
||||
: indicatorBuilder = DefaultChatTimeIndicator.builder,
|
||||
labelResolver = defaultChatTimeIndicatorLabelResolver,
|
||||
sectionCheck = neverShowChatTimeIndicatorSectionChecker;
|
||||
|
||||
/// The general builder for the indicator
|
||||
final ChatTimeIndicatorBuilder indicatorBuilder;
|
||||
|
||||
/// A function that translates offset / time to a string label
|
||||
final ChatTimeIndicatorLabelResolver labelResolver;
|
||||
|
||||
/// A function that determines when a new section starts
|
||||
///
|
||||
/// By default, all messages are prefixed with a message.
|
||||
/// You can disable this using the [skipFirstChatTimeIndicatorSectionChecker]
|
||||
/// instead of the default, which would skip the first section
|
||||
final ChatTimeIndicatorSectionChecker sectionCheck;
|
||||
|
||||
/// public method on the options for readability
|
||||
bool isMessageInNewTimeSection(
|
||||
BuildContext context,
|
||||
MessageModel? previousMessage,
|
||||
MessageModel currentMessage,
|
||||
) =>
|
||||
sectionCheck(
|
||||
context,
|
||||
previousMessage,
|
||||
currentMessage,
|
||||
);
|
||||
}
|
||||
|
||||
/// A function that would generate a string given the current window/datetime
|
||||
typedef ChatTimeIndicatorLabelResolver = String Function(
|
||||
BuildContext context,
|
||||
int dayOffset,
|
||||
DateTime currentWindow,
|
||||
);
|
||||
|
||||
/// A function that would determine if a chat indicator has to render
|
||||
typedef ChatTimeIndicatorSectionChecker = bool Function(
|
||||
BuildContext context,
|
||||
MessageModel? previousMessage,
|
||||
MessageModel currentMessage,
|
||||
);
|
||||
|
||||
/// Build used to render time indicators on chat detail screens
|
||||
typedef ChatTimeIndicatorBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
String timeLabel,
|
||||
);
|
||||
|
||||
///
|
||||
String defaultChatTimeIndicatorLabelResolver(
|
||||
BuildContext context,
|
||||
int dayOffset,
|
||||
DateTime currentWindow,
|
||||
) {
|
||||
var translations = ChatScope.of(context).options.translations;
|
||||
return translations.chatTimeIndicatorLabel(dayOffset, currentWindow);
|
||||
}
|
||||
|
||||
/// A function that disables the time indicator in chat
|
||||
bool neverShowChatTimeIndicatorSectionChecker(
|
||||
BuildContext context,
|
||||
MessageModel? previousMessage,
|
||||
MessageModel currentMessage,
|
||||
) =>
|
||||
false;
|
||||
|
||||
/// Variant of the default implementation for determining if a new section
|
||||
/// starts, where the first section is skipped.
|
||||
///
|
||||
/// Renders a new indicator every new section, skipping the first section
|
||||
bool skipFirstChatTimeIndicatorSectionChecker(
|
||||
BuildContext context,
|
||||
MessageModel? previousMessage,
|
||||
MessageModel currentMessage,
|
||||
) =>
|
||||
previousMessage != null &&
|
||||
previousMessage.timestamp.date.isBefore(currentMessage.timestamp.date);
|
||||
|
||||
/// Default implementation for determining if a new section starts.
|
||||
///
|
||||
/// Renders a new indicator every new section
|
||||
bool defaultChatTimeIndicatorSectionChecker(
|
||||
BuildContext context,
|
||||
MessageModel? previousMessage,
|
||||
MessageModel currentMessage,
|
||||
) =>
|
||||
previousMessage == null ||
|
||||
previousMessage.timestamp.date.isBefore(currentMessage.timestamp.date);
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import "package:intl/intl.dart";
|
||||
|
||||
/// Class that holds all the translations for the chat component view and
|
||||
/// the corresponding userstory
|
||||
class ChatTranslations {
|
||||
|
@ -50,6 +52,7 @@ class ChatTranslations {
|
|||
required this.groupNameEmpty,
|
||||
required this.messagesLoadingError,
|
||||
required this.next,
|
||||
required this.chatTimeIndicatorLabel,
|
||||
});
|
||||
|
||||
/// Default translations for the chat component view
|
||||
|
@ -95,6 +98,8 @@ class ChatTranslations {
|
|||
this.groupNameEmpty = "Group",
|
||||
this.messagesLoadingError = "Error loading messages, you can reload below:",
|
||||
this.next = "Next",
|
||||
this.chatTimeIndicatorLabel =
|
||||
ChatTranslations.defaultChatTimeIndicatorLabel,
|
||||
});
|
||||
|
||||
final String chatsTitle;
|
||||
|
@ -140,6 +145,33 @@ class ChatTranslations {
|
|||
/// to be loaded.
|
||||
final String messagesLoadingError;
|
||||
|
||||
/// The message of a label given a certain offset.
|
||||
///
|
||||
/// The offset determines whether it is today (0), yesterday (-1), or earlier.
|
||||
///
|
||||
/// [dateOffset] will rarely be a +1, however if anyone ever wants to see
|
||||
/// future chat messages, then this number will be positive.
|
||||
///
|
||||
/// use the given [time] format to display exact time information.
|
||||
final String Function(int dateOffset, DateTime time) chatTimeIndicatorLabel;
|
||||
|
||||
/// Standard function to convert an offset to a String.
|
||||
///
|
||||
/// Recommended to always override this in any production app with an
|
||||
/// app localizations implementation.
|
||||
static String defaultChatTimeIndicatorLabel(
|
||||
int dateOffset,
|
||||
DateTime time,
|
||||
) =>
|
||||
switch (dateOffset) {
|
||||
0 => "Today",
|
||||
-1 => "Yesterday",
|
||||
1 => "Tomorrow",
|
||||
int value when value < 5 && value > 1 => "In $value days",
|
||||
int value when value < -1 && value > -5 => "$value days ago",
|
||||
_ => DateFormat("dd-MM-YYYY").format(time),
|
||||
};
|
||||
|
||||
final String next;
|
||||
|
||||
// copyWith method to override the default values
|
||||
|
@ -182,6 +214,7 @@ class ChatTranslations {
|
|||
String? groupNameEmpty,
|
||||
String? messagesLoadingError,
|
||||
String? next,
|
||||
String Function(int dateOffset, DateTime time)? chatTimeIndicatorLabel,
|
||||
}) =>
|
||||
ChatTranslations(
|
||||
chatsTitle: chatsTitle ?? this.chatsTitle,
|
||||
|
@ -234,5 +267,7 @@ class ChatTranslations {
|
|||
groupNameEmpty: groupNameEmpty ?? this.groupNameEmpty,
|
||||
messagesLoadingError: messagesLoadingError ?? this.messagesLoadingError,
|
||||
next: next ?? this.next,
|
||||
chatTimeIndicatorLabel:
|
||||
chatTimeIndicatorLabel ?? this.chatTimeIndicatorLabel,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
import "dart:async";
|
||||
import "dart:typed_data";
|
||||
|
||||
import "package:chat_repository_interface/chat_repository_interface.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_accessibility/flutter_accessibility.dart";
|
||||
import "package:flutter_chat/src/config/chat_options.dart";
|
||||
import "package:flutter_chat/src/config/screen_types.dart";
|
||||
import "package:flutter_chat/flutter_chat.dart";
|
||||
import "package:flutter_chat/src/screens/chat_detail/widgets/chat_bottom.dart";
|
||||
import "package:flutter_chat/src/screens/chat_detail/widgets/chat_widgets.dart";
|
||||
import "package:flutter_chat/src/screens/creation/widgets/default_image_picker.dart";
|
||||
import "package:flutter_chat/src/util/scope.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
|
||||
/// Chat detail screen
|
||||
|
@ -465,14 +462,26 @@ class _ChatBody extends HookWidget {
|
|||
bubbleChildren
|
||||
.add(ChatNoMessages(isGroupChat: chat?.isGroupChat ?? false));
|
||||
} else {
|
||||
for (var (index, msg) in messages.indexed) {
|
||||
var prevMsg = index > 0 ? messages[index - 1] : null;
|
||||
for (var (index, currentMessage) in messages.indexed) {
|
||||
var previousMessage = index > 0 ? messages[index - 1] : null;
|
||||
|
||||
if (options.timeIndicatorOptions.isMessageInNewTimeSection(
|
||||
context,
|
||||
previousMessage,
|
||||
currentMessage,
|
||||
)) {
|
||||
bubbleChildren.add(
|
||||
ChatTimeIndicator(
|
||||
forDate: currentMessage.timestamp,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bubbleChildren.add(
|
||||
ChatBubble(
|
||||
message: msg,
|
||||
previousMessage: prevMsg,
|
||||
sender: userMap[msg.senderId],
|
||||
message: currentMessage,
|
||||
previousMessage: previousMessage,
|
||||
sender: userMap[currentMessage.senderId],
|
||||
onPressSender: onPressUserProfile,
|
||||
semanticIdTitle: options.semantics.chatBubbleTitle(index),
|
||||
semanticIdTime: options.semantics.chatBubbleTime(index),
|
||||
|
|
|
@ -3,6 +3,7 @@ import "package:flutter/material.dart";
|
|||
import "package:flutter_accessibility/flutter_accessibility.dart";
|
||||
import "package:flutter_chat/src/screens/chat_detail/widgets/default_message_builder.dart";
|
||||
import "package:flutter_chat/src/util/scope.dart";
|
||||
import "package:flutter_chat/src/util/utils.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
|
||||
/// Widget displayed when there are no messages in the chat.
|
||||
|
@ -102,3 +103,32 @@ class ChatBubble extends HookWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The indicator above a set of messages, shown per date.
|
||||
class ChatTimeIndicator extends StatelessWidget {
|
||||
/// Creates a ChatTimeIndicator
|
||||
const ChatTimeIndicator({
|
||||
required this.forDate,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The dateTime at which the new time section starts
|
||||
final DateTime forDate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var scope = ChatScope.of(context);
|
||||
var indicatorOptions = scope.options.timeIndicatorOptions;
|
||||
|
||||
var today = DateTime.now();
|
||||
var differenceInDays = today.getDateOffsetInDays(forDate);
|
||||
|
||||
var message = indicatorOptions.labelResolver(
|
||||
context,
|
||||
differenceInDays,
|
||||
forDate,
|
||||
);
|
||||
|
||||
return indicatorOptions.indicatorBuilder(context, message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_chat/flutter_chat.dart";
|
||||
|
||||
/// The default layout for a chat indicator
|
||||
class DefaultChatTimeIndicator extends StatelessWidget {
|
||||
/// Create a default timeindicator in a chat
|
||||
const DefaultChatTimeIndicator({
|
||||
required this.timeIndicatorString,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The text shown in the time indicator
|
||||
final String timeIndicatorString;
|
||||
|
||||
/// Standard builder for time indication
|
||||
static Widget builder(BuildContext context, String timeIndicatorString) =>
|
||||
DefaultChatTimeIndicator(timeIndicatorString: timeIndicatorString);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
var spacing = ChatScope.of(context).options.spacing;
|
||||
return Center(
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(top: spacing.chatBetweenMessagesPadding),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
child: Text(
|
||||
timeIndicatorString,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1 +1,18 @@
|
|||
// add generic utils that are used in the package
|
||||
|
||||
/// Extension to simplify detecting how many days relative dates are
|
||||
extension RelativeDates on DateTime {
|
||||
/// Strips timezone information whilst keeping the exact same date
|
||||
DateTime get utcDate => DateTime.utc(year, month, day);
|
||||
|
||||
/// Strips time information from the date
|
||||
DateTime get date => DateTime(year, month, day);
|
||||
|
||||
/// Get relative date in offset from the current position.
|
||||
///
|
||||
/// `today.getDateOffsetInDays(yesterday)` would result in `-1`
|
||||
///
|
||||
/// `yesterday.getDateOffsetInDays(tomorrow)` would result in `2`
|
||||
int getDateOffsetInDays(DateTime other) =>
|
||||
other.utcDate.difference(utcDate).inDays;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: flutter_chat
|
||||
description: "User story of the chat domain for quick integration into flutter apps"
|
||||
version: 5.0.0
|
||||
version: 5.1.0
|
||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
|
||||
|
||||
environment:
|
||||
|
@ -26,7 +26,7 @@ dependencies:
|
|||
version: ^1.6.0
|
||||
chat_repository_interface:
|
||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
version: ^5.0.0
|
||||
version: ^5.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
14
packages/flutter_chat/test/relative_date_test.dart
Normal file
14
packages/flutter_chat/test/relative_date_test.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
import "package:flutter_chat/src/util/utils.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
void main() {
|
||||
group("RelativeDates", () {
|
||||
test("getDateOffsetInDays", () {
|
||||
var dateA = DateTime(2024, 10, 30);
|
||||
var dateB = DateTime(2024, 10, 01);
|
||||
|
||||
expect(dateA.getDateOffsetInDays(dateB), equals(29));
|
||||
expect(dateB.getDateOffsetInDays(dateA), equals(-29));
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue