import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; import '../../shared/l10n/l10n.dart'; // Re-export shortPubkey so existing callers continue to compile. export '../../shared/utils/string_utils.dart' show shortPubkey; final _fullDateFormat = DateFormat('EEEE, MMMM d, y'); final _shortMonthFormat = DateFormat('MMM'); final _messageTimeFormat = DateFormat('h:mm a', 'en_US'); /// Returns "Today", "Yesterday", or a full date like "Monday, March 31, 2026". /// /// [now] is exposed for testing; production callers should omit it. String formatDayHeading( int unixSeconds, { @visibleForTesting DateTime? now, AppLocalizations? l10n, }) { final date = DateTime.fromMillisecondsSinceEpoch( unixSeconds * 1000, isUtc: true, ).toLocal(); now ??= DateTime.now(); final today = DateTime(now.year, now.month, now.day); final messageDay = DateTime(date.year, date.month, date.day); if (today.year == messageDay.year && today.month == messageDay.month && today.day == messageDay.day) { return l10n?.timeToday ?? 'Today'; } final yesterday = DateTime(now.year, now.month, now.day - 1); if (yesterday.year == messageDay.year && yesterday.month == messageDay.month && yesterday.day == messageDay.day) { return l10n?.timeYesterday ?? 'Yesterday'; } if (l10n != null) { return l10n.fullDate( _weekdayLabel(l10n, date.weekday), _monthLabel(l10n, date.month), date.day, date.year, ); } return _fullDateFormat.format(date); } /// Whether two unix-second timestamps fall on the same calendar day (local time). bool isSameDay(int a, int b) { final dtA = DateTime.fromMillisecondsSinceEpoch( a * 1000, isUtc: true, ).toLocal(); final dtB = DateTime.fromMillisecondsSinceEpoch( b * 1000, isUtc: true, ).toLocal(); return dtA.year == dtB.year && dtA.month == dtB.month && dtA.day == dtB.day; } /// Returns a compact relative time string like "just now", "5m ago", "3h ago", /// "2d ago", or a short date for older timestamps. String relativeTime(int unixSeconds, {AppLocalizations? l10n}) { final now = DateTime.now(); final time = DateTime.fromMillisecondsSinceEpoch( unixSeconds * 1000, isUtc: true, ).toLocal(); final diff = now.difference(time); if (diff.inMinutes < 1) return l10n?.timeJustNow ?? 'just now'; if (diff.inMinutes < 60) { return l10n == null ? '${diff.inMinutes}m ago' : l10n.relativeMinutesAgo(diff.inMinutes); } if (diff.inHours < 24) { return l10n == null ? '${diff.inHours}h ago' : l10n.relativeHoursAgo(diff.inHours); } if (diff.inDays < 7) { return l10n == null ? '${diff.inDays}d ago' : l10n.relativeDaysAgo(diff.inDays); } if (l10n != null) { return l10n.shortDate( _monthLabel(l10n, time.month), time.day, ); } return '${time.month}/${time.day}/${time.year}'; } /// Returns desktop-parity thread activity copy such as "just now", /// "3 hours ago", or "on May 19th". String formatThreadSummaryLastReplyTime( int unixSeconds, { @visibleForTesting int? nowSeconds, AppLocalizations? l10n, }) { nowSeconds ??= DateTime.now().millisecondsSinceEpoch ~/ 1000; var diff = nowSeconds - unixSeconds; if (diff < 0) diff = 0; if (diff < 60) return l10n?.timeJustNow ?? 'just now'; if (diff < 3600) { final value = diff ~/ 60; return l10n == null ? _formatAgo(value, 'minute') : l10n.relativeMinutesAgo(value); } if (diff < 86400) { final value = diff ~/ 3600; return l10n == null ? _formatAgo(value, 'hour') : l10n.relativeHoursAgo(value); } if (diff < 604800) { final value = diff ~/ 86400; return l10n == null ? _formatAgo(value, 'day') : l10n.relativeDaysAgo(value); } final date = DateTime.fromMillisecondsSinceEpoch( unixSeconds * 1000, isUtc: true, ).toLocal(); if (l10n != null) { return l10n.threadOnDate( _monthLabel(l10n, date.month), date.day, l10n.localeName == 'zh_CN' ? '' : _ordinalSuffix(date.day), ); } return 'on ${_shortMonthFormat.format(date)} ' '${date.day}${_ordinalSuffix(date.day)}'; } String _formatAgo(int value, String unit) => '$value $unit${value == 1 ? '' : 's'} ago'; String _ordinalSuffix(int day) { final lastTwoDigits = day % 100; if (lastTwoDigits >= 11 && lastTwoDigits <= 13) return 'th'; return switch (day % 10) { 1 => 'st', 2 => 'nd', 3 => 'rd', _ => 'th', }; } /// Desktop-parity message clock time, e.g. "2:34 PM". String formatMessageTime(int unixSeconds, {AppLocalizations? l10n}) { final date = DateTime.fromMillisecondsSinceEpoch( unixSeconds * 1000, isUtc: true, ).toLocal(); if (l10n == null) return _messageTimeFormat.format(date); final hour = date.hour % 12 == 0 ? 12 : date.hour % 12; final minute = date.minute.toString().padLeft(2, '0'); final period = date.hour < 12 ? l10n.timeAm : l10n.timePm; return l10n.messageTime('$hour', minute, period); } String _monthLabel(AppLocalizations l10n, int month) => switch (month) { 1 => l10n.monthJan, 2 => l10n.monthFeb, 3 => l10n.monthMar, 4 => l10n.monthApr, 5 => l10n.monthMay, 6 => l10n.monthJun, 7 => l10n.monthJul, 8 => l10n.monthAug, 9 => l10n.monthSep, 10 => l10n.monthOct, 11 => l10n.monthNov, _ => l10n.monthDec, }; String _weekdayLabel(AppLocalizations l10n, int weekday) => switch (weekday) { DateTime.monday => l10n.weekdayMonday, DateTime.tuesday => l10n.weekdayTuesday, DateTime.wednesday => l10n.weekdayWednesday, DateTime.thursday => l10n.weekdayThursday, DateTime.friday => l10n.weekdayFriday, DateTime.saturday => l10n.weekdaySaturday, _ => l10n.weekdaySunday, };