251 lines
9.2 KiB
Dart
251 lines
9.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../models/conversation_model.dart';
|
|
import '../theme/app_theme.dart';
|
|
|
|
class ConversationTile extends StatelessWidget {
|
|
final ConversationModel conversation;
|
|
final VoidCallback onTap;
|
|
|
|
const ConversationTile({
|
|
super.key,
|
|
required this.conversation,
|
|
required this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final lastMsg = conversation.lastMessage;
|
|
final hasUnread = conversation.unreadCount > 0;
|
|
final isDark = AppTheme.isDark(context);
|
|
|
|
return InkWell(
|
|
onTap: onTap,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
child: Row(
|
|
children: [
|
|
// ── Avatar ──────────────────────────────────────────────────────
|
|
_buildAvatar(context, conversation),
|
|
const SizedBox(width: 12),
|
|
|
|
// ── Content ─────────────────────────────────────────────────────
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Name row + time
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
conversation.name,
|
|
style: TextStyle(
|
|
color: AppTheme.textPrimary(context),
|
|
fontSize: 16.5,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
_formatTime(conversation.timestamp),
|
|
style: TextStyle(
|
|
color: hasUnread
|
|
? AppTheme.primary
|
|
: AppTheme.textSecondary(context),
|
|
fontSize: 12,
|
|
fontWeight:
|
|
hasUnread ? FontWeight.w600 : FontWeight.normal,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
|
|
// Subtitle row: ack icon + preview + badges
|
|
Row(
|
|
children: [
|
|
// ── ACK icon for sent messages ───────────────────────
|
|
if (lastMsg != null && lastMsg.fromMe) ...[
|
|
_buildAckIcon(context, lastMsg.ack),
|
|
const SizedBox(width: 3),
|
|
],
|
|
|
|
// ── Message preview ──────────────────────────────────
|
|
Expanded(
|
|
child: Text(
|
|
_getSubtitleText(context, lastMsg),
|
|
style: TextStyle(
|
|
color: AppTheme.textSecondary(context),
|
|
fontSize: 14,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
|
|
// ── Trailing badges ──────────────────────────────────
|
|
if (conversation.isMuted) ...[
|
|
const SizedBox(width: 4),
|
|
Icon(Icons.volume_off,
|
|
size: 15, color: AppTheme.textSecondary(context)),
|
|
],
|
|
if (conversation.pinned) ...[
|
|
const SizedBox(width: 4),
|
|
Icon(Icons.push_pin,
|
|
size: 15, color: AppTheme.textSecondary(context)),
|
|
],
|
|
if (hasUnread) ...[
|
|
const SizedBox(width: 6),
|
|
_buildUnreadBadge(conversation.unreadCount),
|
|
],
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Avatar builder (cached network image + fallback initials) ─────────────
|
|
Widget _buildAvatar(BuildContext context, ConversationModel c) {
|
|
final isDark = AppTheme.isDark(context);
|
|
final fallbackBg =
|
|
isDark ? const Color(0xff2a3942) : const Color(0xff6b7c85);
|
|
|
|
if (c.avatar != null && c.avatar!.isNotEmpty) {
|
|
return CircleAvatar(
|
|
radius: 28,
|
|
backgroundColor: fallbackBg,
|
|
child: ClipOval(
|
|
child: CachedNetworkImage(
|
|
imageUrl: c.avatar!,
|
|
width: 56,
|
|
height: 56,
|
|
fit: BoxFit.cover,
|
|
placeholder: (_, __) => _initialsAvatar(c.name, fallbackBg),
|
|
errorWidget: (_, __, ___) => _initialsAvatar(c.name, fallbackBg),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Group icon or person icon
|
|
if (c.isGroup) {
|
|
return CircleAvatar(
|
|
radius: 28,
|
|
backgroundColor: fallbackBg,
|
|
child: const Icon(Icons.group, color: Colors.white, size: 30),
|
|
);
|
|
}
|
|
|
|
return CircleAvatar(
|
|
radius: 28,
|
|
backgroundColor: fallbackBg,
|
|
child: _initialsAvatar(c.name, fallbackBg),
|
|
);
|
|
}
|
|
|
|
Widget _initialsAvatar(String name, Color bg) {
|
|
return Container(
|
|
width: 56,
|
|
height: 56,
|
|
color: bg,
|
|
alignment: Alignment.center,
|
|
child: Icon(
|
|
Icons.person,
|
|
color: Colors.white,
|
|
size: 30,
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Unread badge ──────────────────────────────────────────────────────────
|
|
Widget _buildUnreadBadge(int count) {
|
|
return Container(
|
|
constraints: const BoxConstraints(minWidth: 20, minHeight: 20),
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: const BoxDecoration(
|
|
color: AppTheme.primary,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Text(
|
|
count > 99 ? '99+' : count.toString(),
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 11.5,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── ACK (delivery status) icon ────────────────────────────────────────────
|
|
// Real WhatsApp ACK levels from whatsapp-web.js:
|
|
// -1 = error → clock (pending/error)
|
|
// 0 = pending → clock
|
|
// 1 = sent → single grey tick
|
|
// 2 = received → double grey tick
|
|
// 3 = read/played→ double blue tick
|
|
Widget _buildAckIcon(BuildContext context, int ack) {
|
|
switch (ack) {
|
|
case -1:
|
|
case 0:
|
|
// Pending / clock
|
|
return Icon(Icons.access_time_rounded,
|
|
size: 14, color: AppTheme.textSecondary(context));
|
|
case 1:
|
|
// Sent — single grey tick
|
|
return Icon(Icons.check_rounded,
|
|
size: 15, color: AppTheme.textSecondary(context));
|
|
case 2:
|
|
// Delivered — double grey tick
|
|
return Icon(Icons.done_all_rounded,
|
|
size: 15, color: AppTheme.textSecondary(context));
|
|
case 3:
|
|
// Read — double blue tick
|
|
return const Icon(Icons.done_all_rounded,
|
|
size: 15, color: AppTheme.blueTick);
|
|
default:
|
|
return const SizedBox.shrink();
|
|
}
|
|
}
|
|
|
|
// ── Subtitle text ─────────────────────────────────────────────────────────
|
|
String _getSubtitleText(BuildContext context, LastMessageModel? lastMsg) {
|
|
if (lastMsg == null) return '';
|
|
if (lastMsg.hasMedia) {
|
|
return '📷 Photo';
|
|
}
|
|
return lastMsg.body;
|
|
}
|
|
|
|
// ── Time formatter ────────────────────────────────────────────────────────
|
|
String _formatTime(int timestamp) {
|
|
if (timestamp == 0) return '';
|
|
final dt = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
|
final now = DateTime.now();
|
|
final today = DateTime(now.year, now.month, now.day);
|
|
final yesterday = today.subtract(const Duration(days: 1));
|
|
final msgDate = DateTime(dt.year, dt.month, dt.day);
|
|
|
|
if (msgDate == today) {
|
|
return DateFormat('h:mm a').format(dt);
|
|
} else if (msgDate == yesterday) {
|
|
return 'Yesterday';
|
|
} else if (now.difference(dt).inDays < 7) {
|
|
return DateFormat('EEEE').format(dt);
|
|
} else {
|
|
return DateFormat('dd/MM/yy').format(dt);
|
|
}
|
|
}
|
|
}
|