feat: Batch 1 channel/contacts UX (#2)

- channel_chat: always-visible HH:MM timestamps (theme-aware dark-mode contrast)
- channel_chat: Today/Yesterday/date day dividers between message days
- contacts: pin favorites to the top, preserving the active sort within each group

Fork of zjs81/meshcore-open (MIT). Tracking #2 (migrated from Strycher/LoRa#343).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chore/offband-rebrand
Strycher 1 month ago
parent 33b3b04294
commit 92a6caa409

@ -422,6 +422,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final isUnreadAnchor = final isUnreadAnchor =
_unreadDividerMessageId != null && _unreadDividerMessageId != null &&
message.messageId == _unreadDividerMessageId; message.messageId == _unreadDividerMessageId;
// Day divider above the first message of each
// calendar day. List is reversed, so the older
// neighbour is at index+1.
final olderIdx = messageIndex + 1;
final showDayDivider =
olderIdx >= reversedMessages.length ||
!_isSameDay(
message.timestamp,
reversedMessages[olderIdx].timestamp,
);
return Container( return Container(
key: _messageKeys[message.messageId]!, key: _messageKeys[message.messageId]!,
child: Builder( child: Builder(
@ -434,13 +444,21 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
message, message,
textScale, textScale,
); );
if (isUnreadAnchor) { if (!showDayDivider && !isUnreadAnchor) {
return bubble;
}
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [const UnreadDivider(), bubble], children: [
if (showDayDivider)
_buildDayDivider(
context,
message.timestamp,
),
if (isUnreadAnchor) const UnreadDivider(),
bubble,
],
); );
}
return bubble;
}, },
), ),
); );
@ -702,7 +720,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_formatTime(context, message.timestamp), _formatTime(context, message.timestamp),
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Colors.grey[600], color:
Theme.of(context).brightness ==
Brightness.dark
? Colors.grey[400]
: Colors.grey[600],
), ),
), ),
if (message.repeatCount > 0) ...[ if (message.repeatCount > 0) ...[
@ -1360,22 +1382,59 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
); );
} }
String _formatTime(BuildContext context, DateTime time) { void _ensureDateFormats(BuildContext context) {
final now = DateTime.now();
final diff = now.difference(time);
final locale = Localizations.localeOf(context).toString(); final locale = Localizations.localeOf(context).toString();
if (locale != _cachedFormatLocale) { if (locale != _cachedFormatLocale) {
_cachedFormatLocale = locale; _cachedFormatLocale = locale;
_hmFormat = DateFormat.Hm(locale); _hmFormat = DateFormat.Hm(locale);
_mdFormat = DateFormat.Md(locale); _mdFormat = DateFormat.Md(locale);
} }
final hm = _hmFormat.format(time); }
if (diff.inDays > 0) { String _formatTime(BuildContext context, DateTime time) {
return '${_mdFormat.format(time)} $hm'; _ensureDateFormats(context);
} else { // Always show absolute HH:mm; the calendar date is shown via day dividers.
return hm; return _hmFormat.format(time);
} }
bool _isSameDay(DateTime a, DateTime b) =>
a.year == b.year && a.month == b.month && a.day == b.day;
/// Date header rendered above the first message of each calendar day.
Widget _buildDayDivider(BuildContext context, DateTime time) {
_ensureDateFormats(context);
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final daysAgo =
today.difference(DateTime(time.year, time.month, time.day)).inDays;
final label = daysAgo == 0
? 'Today'
: daysAgo == 1
? 'Yesterday'
: _mdFormat.format(time);
final color = Theme.of(context).brightness == Brightness.dark
? Colors.grey[400]
: Colors.grey[700];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Row(
children: [
Expanded(child: Divider(color: color?.withValues(alpha: 0.25))),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: color,
),
),
),
Expanded(child: Divider(color: color?.withValues(alpha: 0.25))),
],
),
);
} }
void _showMessagePathInfo(ChannelMessage message) { void _showMessagePathInfo(ChannelMessage message) {

@ -902,6 +902,12 @@ class _ContactsScreenState extends State<ContactsScreen>
break; break;
} }
// Favorites pinned to the top, keeping the selected sort order within each group.
final favorites = filtered.where((c) => c.isFavorite).toList();
if (favorites.isNotEmpty && favorites.length < filtered.length) {
filtered = [...favorites, ...filtered.where((c) => !c.isFavorite)];
}
return filtered; return filtered;
} }

Loading…
Cancel
Save

Powered by TurnKey Linux.