mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 10:58:40 +00:00
add time to messages
This commit is contained in:
parent
b03bcfe6e1
commit
edf4209448
11 changed files with 260 additions and 54 deletions
|
|
@ -175,6 +175,11 @@
|
|||
"close": "Schließen",
|
||||
"cancel": "Abbrechen",
|
||||
"ok": "Ok",
|
||||
"now": "Jetzt",
|
||||
"you": "Du",
|
||||
"minutesShort": "Min.",
|
||||
"image": "Bild",
|
||||
"video": "Video",
|
||||
"react": "Reagieren",
|
||||
"reply": "Antworten",
|
||||
"copy": "Kopieren",
|
||||
|
|
|
|||
|
|
@ -299,6 +299,11 @@
|
|||
"disable": "Disable",
|
||||
"enable": "Enable",
|
||||
"cancel": "Cancel",
|
||||
"now": "Now",
|
||||
"you": "You",
|
||||
"minutesShort": "min.",
|
||||
"image": "Image",
|
||||
"video": "Video",
|
||||
"react": "React",
|
||||
"reply": "Reply",
|
||||
"copy": "Copy",
|
||||
|
|
|
|||
|
|
@ -1058,6 +1058,36 @@ abstract class AppLocalizations {
|
|||
/// **'Cancel'**
|
||||
String get cancel;
|
||||
|
||||
/// No description provided for @now.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Now'**
|
||||
String get now;
|
||||
|
||||
/// No description provided for @you.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'You'**
|
||||
String get you;
|
||||
|
||||
/// No description provided for @minutesShort.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'min.'**
|
||||
String get minutesShort;
|
||||
|
||||
/// No description provided for @image.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Image'**
|
||||
String get image;
|
||||
|
||||
/// No description provided for @video.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Video'**
|
||||
String get video;
|
||||
|
||||
/// No description provided for @react.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -536,6 +536,21 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get cancel => 'Abbrechen';
|
||||
|
||||
@override
|
||||
String get now => 'Jetzt';
|
||||
|
||||
@override
|
||||
String get you => 'Du';
|
||||
|
||||
@override
|
||||
String get minutesShort => 'Min.';
|
||||
|
||||
@override
|
||||
String get image => 'Bild';
|
||||
|
||||
@override
|
||||
String get video => 'Video';
|
||||
|
||||
@override
|
||||
String get react => 'Reagieren';
|
||||
|
||||
|
|
|
|||
|
|
@ -531,6 +531,21 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get cancel => 'Cancel';
|
||||
|
||||
@override
|
||||
String get now => 'Now';
|
||||
|
||||
@override
|
||||
String get you => 'You';
|
||||
|
||||
@override
|
||||
String get minutesShort => 'min.';
|
||||
|
||||
@override
|
||||
String get image => 'Image';
|
||||
|
||||
@override
|
||||
String get video => 'Video';
|
||||
|
||||
@override
|
||||
String get react => 'React';
|
||||
|
||||
|
|
|
|||
|
|
@ -28,22 +28,17 @@ Color getMessageColor(Message message) {
|
|||
}
|
||||
|
||||
class ChatItem {
|
||||
const ChatItem._({this.message, this.date, this.time});
|
||||
const ChatItem._({this.message, this.date});
|
||||
factory ChatItem.date(DateTime date) {
|
||||
return ChatItem._(date: date);
|
||||
}
|
||||
factory ChatItem.time(DateTime time) {
|
||||
return ChatItem._(time: time);
|
||||
}
|
||||
factory ChatItem.message(Message message) {
|
||||
return ChatItem._(message: message);
|
||||
}
|
||||
final Message? message;
|
||||
final DateTime? date;
|
||||
final DateTime? time;
|
||||
bool get isMessage => message != null;
|
||||
bool get isDate => date != null;
|
||||
bool get isTime => time != null;
|
||||
}
|
||||
|
||||
/// Displays detailed information about a SampleItem.
|
||||
|
|
@ -143,9 +138,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
msg.createdAt.year != lastDate.year) {
|
||||
chatItems.add(ChatItem.date(msg.createdAt));
|
||||
lastDate = msg.createdAt;
|
||||
} else if (msg.createdAt.difference(lastDate).inMinutes >= 20) {
|
||||
chatItems.add(ChatItem.time(msg.createdAt));
|
||||
lastDate = msg.createdAt;
|
||||
}
|
||||
chatItems.add(ChatItem.message(msg));
|
||||
}
|
||||
|
|
@ -276,7 +268,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
padding: EdgeInsetsGeometry.only(top: 10),
|
||||
);
|
||||
}
|
||||
if (messages[i].isDate || messages[i].isTime) {
|
||||
if (messages[i].isDate) {
|
||||
return ChatDateChip(
|
||||
item: messages[i],
|
||||
);
|
||||
|
|
@ -295,10 +287,14 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
scale: (focusedScrollItem == i) ? 1.05 : 1,
|
||||
child: ChatListEntry(
|
||||
key: Key(chatMessage.messageId),
|
||||
chatMessage,
|
||||
group,
|
||||
galleryItems,
|
||||
isLastMessageFromSameUser(messages, i),
|
||||
message: messages[i].message!,
|
||||
nextMessage:
|
||||
(i > 0) ? messages[i - 1].message : null,
|
||||
prevMessage: ((i + 1) < messages.length)
|
||||
? messages[i + 1].message
|
||||
: null,
|
||||
group: group,
|
||||
galleryItems: galleryItems,
|
||||
scrollToMessage: scrollToMessage,
|
||||
onResponseTriggered: () {
|
||||
setState(() {
|
||||
|
|
@ -403,22 +399,3 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool isLastMessageFromSameUser(List<ChatItem> messages, int index) {
|
||||
if (index <= 0) {
|
||||
return true; // If there is no previous message, return true
|
||||
}
|
||||
return (messages[index - 1].message?.senderId ==
|
||||
messages[index].message?.senderId);
|
||||
}
|
||||
|
||||
double calculateNumberOfLines(String text, double width, double fontSize) {
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: text,
|
||||
style: TextStyle(fontSize: fontSize),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout(maxWidth: width - 32);
|
||||
return textPainter.computeLineMetrics().length.toDouble();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,8 @@ class ChatDateChip extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final formattedDate = item.isTime
|
||||
? DateFormat.Hm(Localizations.localeOf(context).toLanguageTag())
|
||||
.format(item.time!)
|
||||
: '${DateFormat.Hm(Localizations.localeOf(context).toLanguageTag()).format(item.date!)}\n${DateFormat.yMd(Localizations.localeOf(context).toLanguageTag()).format(item.date!)}';
|
||||
final formattedDate =
|
||||
'${DateFormat.Hm(Localizations.localeOf(context).toLanguageTag()).format(item.date!)}\n${DateFormat.yMd(Localizations.localeOf(context).toLanguageTag()).format(item.date!)}';
|
||||
|
||||
return Center(
|
||||
child: Container(
|
||||
|
|
@ -23,6 +21,7 @@ class ChatDateChip extends StatelessWidget {
|
|||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
child: Text(
|
||||
formattedDate,
|
||||
textAlign: TextAlign.center,
|
||||
|
|
|
|||
|
|
@ -15,18 +15,20 @@ import 'package:twonly/src/views/chats/chat_messages_components/message_context_
|
|||
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
|
||||
|
||||
class ChatListEntry extends StatefulWidget {
|
||||
const ChatListEntry(
|
||||
this.message,
|
||||
this.group,
|
||||
this.galleryItems,
|
||||
this.lastMessageFromSameUser, {
|
||||
const ChatListEntry({
|
||||
required this.group,
|
||||
required this.galleryItems,
|
||||
required this.prevMessage,
|
||||
required this.message,
|
||||
required this.nextMessage,
|
||||
required this.onResponseTriggered,
|
||||
required this.scrollToMessage,
|
||||
super.key,
|
||||
});
|
||||
final Message? prevMessage;
|
||||
final Message? nextMessage;
|
||||
final Message message;
|
||||
final Group group;
|
||||
final bool lastMessageFromSameUser;
|
||||
final List<MemoryItem> galleryItems;
|
||||
final void Function(String) scrollToMessage;
|
||||
final void Function() onResponseTriggered;
|
||||
|
|
@ -70,12 +72,16 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
Widget build(BuildContext context) {
|
||||
final right = widget.message.senderId == null;
|
||||
|
||||
final (padding, borderRadius) = getMessageLayout(
|
||||
widget.message,
|
||||
widget.prevMessage,
|
||||
widget.nextMessage,
|
||||
);
|
||||
|
||||
return Align(
|
||||
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: widget.lastMessageFromSameUser
|
||||
? const EdgeInsets.only(top: 5, right: 10, left: 10)
|
||||
: const EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10),
|
||||
padding: padding,
|
||||
child: MessageContextMenu(
|
||||
message: widget.message,
|
||||
onResponseTriggered: widget.onResponseTriggered,
|
||||
|
|
@ -96,10 +102,13 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
msg: widget.message,
|
||||
group: widget.group,
|
||||
mediaService: mediaService,
|
||||
borderRadius: borderRadius,
|
||||
scrollToMessage: widget.scrollToMessage,
|
||||
child: (widget.message.type == MessageType.text)
|
||||
? ChatTextEntry(
|
||||
message: widget.message,
|
||||
nextMessage: widget.nextMessage,
|
||||
borderRadius: borderRadius,
|
||||
)
|
||||
: (mediaService == null)
|
||||
? null
|
||||
|
|
@ -128,3 +137,57 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
(EdgeInsetsGeometry, BorderRadius) getMessageLayout(
|
||||
Message message,
|
||||
Message? prevMessage,
|
||||
Message? nextMessage,
|
||||
) {
|
||||
var bottom = 30.0;
|
||||
var top = 0.0;
|
||||
|
||||
var topLeft = 12.0;
|
||||
var topRight = 12.0;
|
||||
var bottomRight = 12.0;
|
||||
var bottomLeft = 12.0;
|
||||
|
||||
if (nextMessage != null) {
|
||||
if (message.senderId == nextMessage.senderId) {
|
||||
bottom = 10;
|
||||
}
|
||||
}
|
||||
|
||||
if (prevMessage != null) {
|
||||
final combinesWidthNext = combineTextMessageWithNext(prevMessage, message);
|
||||
if (combinesWidthNext) {
|
||||
top = 1;
|
||||
topLeft = 5.0;
|
||||
}
|
||||
}
|
||||
|
||||
final combinesWidthNext = combineTextMessageWithNext(message, nextMessage);
|
||||
if (combinesWidthNext) {
|
||||
bottom = 1;
|
||||
bottomLeft = 5.0;
|
||||
}
|
||||
|
||||
if (message.senderId == null) {
|
||||
final tmp = topLeft;
|
||||
topLeft = topRight;
|
||||
topRight = tmp;
|
||||
|
||||
final tmp2 = bottomLeft;
|
||||
bottomLeft = bottomRight;
|
||||
bottomRight = tmp2;
|
||||
}
|
||||
|
||||
return (
|
||||
EdgeInsets.only(top: top, bottom: bottom, right: 10, left: 10),
|
||||
BorderRadius.only(
|
||||
topLeft: Radius.circular(topLeft),
|
||||
topRight: Radius.circular(topRight),
|
||||
bottomRight: Radius.circular(bottomRight),
|
||||
bottomLeft: Radius.circular(bottomLeft),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart' hide TextDirection;
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages.view.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
import 'package:twonly/src/views/components/better_text.dart';
|
||||
|
|
@ -7,10 +10,14 @@ import 'package:twonly/src/views/components/better_text.dart';
|
|||
class ChatTextEntry extends StatelessWidget {
|
||||
const ChatTextEntry({
|
||||
required this.message,
|
||||
required this.nextMessage,
|
||||
required this.borderRadius,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Message message;
|
||||
final Message? nextMessage;
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -27,17 +34,98 @@ class ChatTextEntry extends StatelessWidget {
|
|||
child: EmojiAnimation(emoji: text),
|
||||
);
|
||||
}
|
||||
|
||||
final displayTime = !combineTextMessageWithNext(message, nextMessage);
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 10, top: 4, bottom: 4),
|
||||
padding: const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
message.quotesMessageId == null ? getMessageColor(message) : null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (measureTextWidth(text) > 270)
|
||||
Expanded(
|
||||
child: BetterText(text: text),
|
||||
)
|
||||
else
|
||||
BetterText(text: text),
|
||||
if (displayTime)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 6),
|
||||
child: Text(
|
||||
friendlyTime(context, message.createdAt),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white.withAlpha(150),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: BetterText(text: text),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
double measureTextWidth(
|
||||
String text,
|
||||
) {
|
||||
final tp = TextPainter(
|
||||
text: TextSpan(text: text, style: const TextStyle(fontSize: 17)),
|
||||
textDirection: TextDirection.ltr,
|
||||
maxLines: 1,
|
||||
)..layout();
|
||||
return tp.size.width;
|
||||
}
|
||||
|
||||
bool combineTextMessageWithNext(Message message, Message? nextMessage) {
|
||||
if (nextMessage != null && nextMessage.content != null) {
|
||||
if (nextMessage.senderId == message.senderId) {
|
||||
if (nextMessage.type == MessageType.text &&
|
||||
message.type == MessageType.text) {
|
||||
if (!EmojiAnimation.supported(nextMessage.content!)) {
|
||||
final diff =
|
||||
nextMessage.createdAt.difference(message.createdAt).inMinutes;
|
||||
if (diff <= 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String friendlyTime(BuildContext context, DateTime dt) {
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(dt);
|
||||
|
||||
if (diff.inMinutes >= 0 && diff.inMinutes < 60) {
|
||||
final minutes = diff.inMinutes == 0 ? 1 : diff.inMinutes;
|
||||
if (minutes <= 1) {
|
||||
return context.lang.now;
|
||||
}
|
||||
return '$minutes ${context.lang.minutesShort}';
|
||||
}
|
||||
|
||||
// Determine 24h vs 12h from system/local settings
|
||||
final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat;
|
||||
|
||||
if (!use24Hour) {
|
||||
// 12-hour format with locale-aware AM/PM
|
||||
final format = DateFormat.jm(Localizations.localeOf(context).toString());
|
||||
return format.format(dt);
|
||||
} else {
|
||||
// 24-hour HH:mm, locale-aware
|
||||
final format = DateFormat.Hm(Localizations.localeOf(context).toString());
|
||||
return format.format(dt);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class ResponseContainer extends StatefulWidget {
|
|||
required this.child,
|
||||
required this.scrollToMessage,
|
||||
required this.mediaService,
|
||||
required this.borderRadius,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ class ResponseContainer extends StatefulWidget {
|
|||
final Widget? child;
|
||||
final Group group;
|
||||
final MediaFileService? mediaService;
|
||||
final BorderRadius borderRadius;
|
||||
final void Function(String) scrollToMessage;
|
||||
|
||||
@override
|
||||
|
|
@ -69,7 +71,7 @@ class _ResponseContainerState extends State<ResponseContainer> {
|
|||
),
|
||||
decoration: BoxDecoration(
|
||||
color: getMessageColor(widget.msg),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: widget.borderRadius,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
@ -157,11 +159,12 @@ class _ResponsePreviewState extends State<ResponsePreview> {
|
|||
}
|
||||
}
|
||||
if (message!.type == MessageType.media && mediaService != null) {
|
||||
subtitle =
|
||||
mediaService!.mediaFile.type == MediaType.video ? 'Video' : 'Image';
|
||||
subtitle = mediaService!.mediaFile.type == MediaType.video
|
||||
? context.lang.video
|
||||
: context.lang.image;
|
||||
}
|
||||
|
||||
var username = 'You';
|
||||
var username = context.lang.you;
|
||||
if (message!.senderId != null) {
|
||||
username = message!.senderId.toString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,13 +51,19 @@ class BetterText extends StatelessWidget {
|
|||
}
|
||||
|
||||
if (lastMatchEnd < text.length) {
|
||||
spans.add(TextSpan(text: text.substring(lastMatchEnd)));
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: text.substring(lastMatchEnd),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: spans,
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.visible,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 17,
|
||||
|
|
|
|||
Loading…
Reference in a new issue