mirror of
https://github.com/krille-chan/fluffychat
synced 2024-10-05 17:52:45 +00:00
design: New date separators in timeline
This commit is contained in:
parent
7183158cf9
commit
f15b81c5a6
6 changed files with 134 additions and 79 deletions
|
@ -2485,5 +2485,6 @@
|
||||||
"stickers": "Stickers",
|
"stickers": "Stickers",
|
||||||
"discover": "Discover",
|
"discover": "Discover",
|
||||||
"commandHint_ignore": "Ignore the given matrix ID",
|
"commandHint_ignore": "Ignore the given matrix ID",
|
||||||
"commandHint_unignore": "Unignore the given matrix ID"
|
"commandHint_unignore": "Unignore the given matrix ID",
|
||||||
|
"today": "Today"
|
||||||
}
|
}
|
|
@ -5,12 +5,14 @@ import 'package:scroll_to_index/scroll_to_index.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/themes.dart';
|
import 'package:fluffychat/config/themes.dart';
|
||||||
import 'package:fluffychat/pages/chat/chat.dart';
|
import 'package:fluffychat/pages/chat/chat.dart';
|
||||||
|
import 'package:fluffychat/pages/chat/events/date_separator.dart';
|
||||||
import 'package:fluffychat/pages/chat/events/message.dart';
|
import 'package:fluffychat/pages/chat/events/message.dart';
|
||||||
import 'package:fluffychat/pages/chat/seen_by_row.dart';
|
import 'package:fluffychat/pages/chat/seen_by_row.dart';
|
||||||
import 'package:fluffychat/pages/chat/typing_indicators.dart';
|
import 'package:fluffychat/pages/chat/typing_indicators.dart';
|
||||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||||
import 'package:fluffychat/utils/account_config.dart';
|
import 'package:fluffychat/utils/account_config.dart';
|
||||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||||
|
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||||
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
|
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
|
||||||
import 'package:fluffychat/utils/platform_infos.dart';
|
import 'package:fluffychat/utils/platform_infos.dart';
|
||||||
|
|
||||||
|
@ -106,6 +108,8 @@ class ChatEventList extends StatelessWidget {
|
||||||
|
|
||||||
// The message at this index:
|
// The message at this index:
|
||||||
final event = events[i];
|
final event = events[i];
|
||||||
|
final nextEvent = i + 1 < events.length ? events[i + 1] : null;
|
||||||
|
final previousEvent = i > 0 ? events[i - 1] : null;
|
||||||
final animateIn = animateInEventIndex != null &&
|
final animateIn = animateInEventIndex != null &&
|
||||||
controller.timeline!.events.length > animateInEventIndex &&
|
controller.timeline!.events.length > animateInEventIndex &&
|
||||||
event == controller.timeline!.events[animateInEventIndex];
|
event == controller.timeline!.events[animateInEventIndex];
|
||||||
|
@ -114,39 +118,48 @@ class ChatEventList extends StatelessWidget {
|
||||||
key: ValueKey(event.eventId),
|
key: ValueKey(event.eventId),
|
||||||
index: i,
|
index: i,
|
||||||
controller: controller.scrollController,
|
controller: controller.scrollController,
|
||||||
child: Message(
|
child: Column(
|
||||||
event,
|
mainAxisSize: MainAxisSize.min,
|
||||||
animateIn: animateIn,
|
children: [
|
||||||
resetAnimateIn: () {
|
if (nextEvent?.originServerTs
|
||||||
controller.animateInEventIndex = null;
|
.isSameDate(event.originServerTs) !=
|
||||||
},
|
true)
|
||||||
onSwipe: () => controller.replyAction(replyTo: event),
|
DateSeparator(date: event.originServerTs),
|
||||||
onInfoTab: controller.showEventInfo,
|
Message(
|
||||||
onAvatarTab: (Event event) => showAdaptiveBottomSheet(
|
event,
|
||||||
context: context,
|
animateIn: animateIn,
|
||||||
builder: (c) => UserBottomSheet(
|
resetAnimateIn: () {
|
||||||
user: event.senderFromMemoryOrFallback,
|
controller.animateInEventIndex = null;
|
||||||
outerContext: context,
|
},
|
||||||
onMention: () => controller.sendController.text +=
|
onSwipe: () => controller.replyAction(replyTo: event),
|
||||||
'${event.senderFromMemoryOrFallback.mention} ',
|
onInfoTab: controller.showEventInfo,
|
||||||
|
onAvatarTab: (Event event) => showAdaptiveBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (c) => UserBottomSheet(
|
||||||
|
user: event.senderFromMemoryOrFallback,
|
||||||
|
outerContext: context,
|
||||||
|
onMention: () => controller.sendController.text +=
|
||||||
|
'${event.senderFromMemoryOrFallback.mention} ',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
highlightMarker:
|
||||||
|
controller.scrollToEventIdMarker == event.eventId,
|
||||||
|
onSelect: controller.onSelectMessage,
|
||||||
|
scrollToEventId: (String eventId) =>
|
||||||
|
controller.scrollToEventId(eventId),
|
||||||
|
longPressSelect: controller.selectedEvents.isNotEmpty,
|
||||||
|
selected: controller.selectedEvents
|
||||||
|
.any((e) => e.eventId == event.eventId),
|
||||||
|
timeline: controller.timeline!,
|
||||||
|
displayReadMarker:
|
||||||
|
controller.readMarkerEventId == event.eventId &&
|
||||||
|
controller.timeline?.allowNewEvent == false,
|
||||||
|
nextEvent: nextEvent,
|
||||||
|
previousEvent: previousEvent,
|
||||||
|
avatarPresenceBackgroundColor:
|
||||||
|
hasWallpaper ? Colors.transparent : null,
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
highlightMarker:
|
|
||||||
controller.scrollToEventIdMarker == event.eventId,
|
|
||||||
onSelect: controller.onSelectMessage,
|
|
||||||
scrollToEventId: (String eventId) =>
|
|
||||||
controller.scrollToEventId(eventId),
|
|
||||||
longPressSelect: controller.selectedEvents.isNotEmpty,
|
|
||||||
selected: controller.selectedEvents
|
|
||||||
.any((e) => e.eventId == event.eventId),
|
|
||||||
timeline: controller.timeline!,
|
|
||||||
displayReadMarker:
|
|
||||||
controller.readMarkerEventId == event.eventId &&
|
|
||||||
controller.timeline?.allowNewEvent == false,
|
|
||||||
nextEvent: i + 1 < events.length ? events[i + 1] : null,
|
|
||||||
previousEvent: i > 0 ? events[i - 1] : null,
|
|
||||||
avatarPresenceBackgroundColor:
|
|
||||||
hasWallpaper ? Colors.transparent : null,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
42
lib/pages/chat/events/date_separator.dart
Normal file
42
lib/pages/chat/events/date_separator.dart
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
|
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||||
|
|
||||||
|
class DateSeparator extends StatelessWidget {
|
||||||
|
final DateTime date;
|
||||||
|
const DateSeparator({required this.date, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 1,
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Text(
|
||||||
|
date.localizedTimeShort(context, dateOnly: true),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
fontSize: 13 * AppConfig.fontSizeFactor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 1,
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -203,27 +203,53 @@ class Message extends StatelessWidget {
|
||||||
if (!nextEventSameSender)
|
if (!nextEventSameSender)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0, bottom: 4),
|
padding: const EdgeInsets.only(left: 8.0, bottom: 4),
|
||||||
child: ownMessage || event.room.isDirectChat
|
child: Row(
|
||||||
? const SizedBox(height: 12)
|
mainAxisAlignment: ownMessage
|
||||||
: FutureBuilder<User?>(
|
? MainAxisAlignment.end
|
||||||
|
: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (ownMessage || event.room.isDirectChat)
|
||||||
|
const SizedBox(height: 12)
|
||||||
|
else
|
||||||
|
FutureBuilder<User?>(
|
||||||
future: event.fetchSenderUser(),
|
future: event.fetchSenderUser(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final displayname =
|
final displayname =
|
||||||
snapshot.data?.calcDisplayname() ??
|
snapshot.data?.calcDisplayname() ??
|
||||||
event.senderFromMemoryOrFallback
|
event.senderFromMemoryOrFallback
|
||||||
.calcDisplayname();
|
.calcDisplayname();
|
||||||
return Text(
|
return ConstrainedBox(
|
||||||
displayname,
|
constraints: const BoxConstraints(
|
||||||
style: TextStyle(
|
maxWidth: FluffyThemes.columnWidth / 2,
|
||||||
fontSize: 12,
|
),
|
||||||
color: (Theme.of(context).brightness ==
|
child: Text(
|
||||||
Brightness.light
|
displayname,
|
||||||
? displayname.color
|
maxLines: 1,
|
||||||
: displayname.lightColorText),
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: (Theme.of(context).brightness ==
|
||||||
|
Brightness.light
|
||||||
|
? displayname.color
|
||||||
|
: displayname.lightColorText),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
(ownMessage || event.room.isDirectChat
|
||||||
|
? ''
|
||||||
|
: ' | ') +
|
||||||
|
event.originServerTs
|
||||||
|
.localizedTimeOfDay(context),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12 * AppConfig.fontSizeFactor,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
|
@ -363,38 +389,12 @@ class Message extends StatelessWidget {
|
||||||
Widget container;
|
Widget container;
|
||||||
final showReceiptsRow =
|
final showReceiptsRow =
|
||||||
event.hasAggregatedEvents(timeline, RelationshipTypes.reaction);
|
event.hasAggregatedEvents(timeline, RelationshipTypes.reaction);
|
||||||
if (showReceiptsRow || displayTime || selected || displayReadMarker) {
|
if (showReceiptsRow || selected || displayReadMarker) {
|
||||||
container = Column(
|
container = Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment:
|
crossAxisAlignment:
|
||||||
ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
if (displayTime || selected)
|
|
||||||
Padding(
|
|
||||||
padding: displayTime
|
|
||||||
? const EdgeInsets.symmetric(vertical: 8.0)
|
|
||||||
: EdgeInsets.zero,
|
|
||||||
child: Center(
|
|
||||||
child: Material(
|
|
||||||
color: displayTime
|
|
||||||
? Theme.of(context).colorScheme.background
|
|
||||||
: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.background
|
|
||||||
.withOpacity(0.33),
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(AppConfig.borderRadius / 2),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(4.0),
|
|
||||||
child: Text(
|
|
||||||
event.originServerTs.localizedTime(context),
|
|
||||||
style: TextStyle(fontSize: 13 * AppConfig.fontSizeFactor),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
row,
|
row,
|
||||||
AnimatedSize(
|
AnimatedSize(
|
||||||
duration: FluffyThemes.animationDuration,
|
duration: FluffyThemes.animationDuration,
|
||||||
|
|
|
@ -187,12 +187,7 @@ class ChatListView extends StatelessWidget {
|
||||||
? NavigationBar(
|
? NavigationBar(
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
labelBehavior:
|
labelBehavior:
|
||||||
NavigationDestinationLabelBehavior.alwaysHide,
|
NavigationDestinationLabelBehavior.alwaysShow,
|
||||||
height: 64,
|
|
||||||
shadowColor:
|
|
||||||
Theme.of(context).colorScheme.onBackground,
|
|
||||||
surfaceTintColor:
|
|
||||||
Theme.of(context).colorScheme.background,
|
|
||||||
selectedIndex: controller.selectedIndex,
|
selectedIndex: controller.selectedIndex,
|
||||||
onDestinationSelected:
|
onDestinationSelected:
|
||||||
controller.onDestinationSelected,
|
controller.onDestinationSelected,
|
||||||
|
|
|
@ -45,7 +45,7 @@ extension DateTimeExtension on DateTime {
|
||||||
|
|
||||||
/// Returns [localizedTimeOfDay()] if the ChatTime is today, the name of the week
|
/// Returns [localizedTimeOfDay()] if the ChatTime is today, the name of the week
|
||||||
/// day if the ChatTime is this week and a date string else.
|
/// day if the ChatTime is this week and a date string else.
|
||||||
String localizedTimeShort(BuildContext context) {
|
String localizedTimeShort(BuildContext context, {bool dateOnly = false}) {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
final sameYear = now.year == year;
|
final sameYear = now.year == year;
|
||||||
|
@ -58,7 +58,7 @@ extension DateTimeExtension on DateTime {
|
||||||
1000 * 60 * 60 * 24 * 7;
|
1000 * 60 * 60 * 24 * 7;
|
||||||
|
|
||||||
if (sameDay) {
|
if (sameDay) {
|
||||||
return localizedTimeOfDay(context);
|
return dateOnly ? L10n.of(context)!.today : localizedTimeOfDay(context);
|
||||||
} else if (sameWeek) {
|
} else if (sameWeek) {
|
||||||
return DateFormat.EEEE(Localizations.localeOf(context).languageCode)
|
return DateFormat.EEEE(Localizations.localeOf(context).languageCode)
|
||||||
.format(this);
|
.format(this);
|
||||||
|
@ -92,5 +92,9 @@ extension DateTimeExtension on DateTime {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isSameDate(DateTime other) {
|
||||||
|
return year == other.year && month == other.month && day == other.day;
|
||||||
|
}
|
||||||
|
|
||||||
static String _z(int i) => i < 10 ? '0${i.toString()}' : i.toString();
|
static String _z(int i) => i < 10 ? '0${i.toString()}' : i.toString();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue