Sync update: 2026-05-19 23:27:14
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -17,117 +18,217 @@ class ConversationTile extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final lastMsg = conversation.lastMessage;
|
||||
final hasUnread = conversation.unreadCount > 0;
|
||||
final isDark = AppTheme.isDark(context);
|
||||
|
||||
return ListTile(
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
leading: _buildAvatar(),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
conversation.name,
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_formatTime(conversation.timestamp),
|
||||
style: TextStyle(
|
||||
color: hasUnread ? AppTheme.primary : AppTheme.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: hasUnread ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
if (lastMsg != null && lastMsg.fromMe) ...[
|
||||
const Icon(Icons.done_all, size: 16, color: AppTheme.primary), // Or proper ACK double tick
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
// ── Avatar ──────────────────────────────────────────────────────
|
||||
_buildAvatar(context, conversation),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// ── Content ─────────────────────────────────────────────────────
|
||||
Expanded(
|
||||
child: Text(
|
||||
_getSubtitleText(lastMsg),
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
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),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (conversation.isMuted) ...[
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.volume_off, size: 16, color: AppTheme.textSecondary),
|
||||
],
|
||||
if (conversation.pinned) ...[
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.push_pin, size: 16, color: AppTheme.textSecondary),
|
||||
],
|
||||
if (hasUnread) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppTheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Text(
|
||||
conversation.unreadCount.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAvatar() {
|
||||
if (conversation.avatar != null) {
|
||||
// ── 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: 26,
|
||||
backgroundImage: NetworkImage(conversation.avatar!),
|
||||
backgroundColor: AppTheme.surfaceLight,
|
||||
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: 26,
|
||||
backgroundColor: AppTheme.primaryDark,
|
||||
child: Text(
|
||||
conversation.name.isNotEmpty ? conversation.name[0].toUpperCase() : '?',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getSubtitleText(LastMessageModel? lastMsg) {
|
||||
// ── 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'; // or other media indicator
|
||||
return '📷 Photo';
|
||||
}
|
||||
return lastMsg.body;
|
||||
}
|
||||
|
||||
// ── Time formatter ────────────────────────────────────────────────────────
|
||||
String _formatTime(int timestamp) {
|
||||
if (timestamp == 0) return '';
|
||||
final dt = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
||||
@@ -137,13 +238,13 @@ class ConversationTile extends StatelessWidget {
|
||||
final msgDate = DateTime(dt.year, dt.month, dt.day);
|
||||
|
||||
if (msgDate == today) {
|
||||
return DateFormat('hh:mm a').format(dt);
|
||||
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); // e.g. "Monday"
|
||||
return DateFormat('EEEE').format(dt);
|
||||
} else {
|
||||
return DateFormat('MM/dd/yy').format(dt);
|
||||
return DateFormat('dd/MM/yy').format(dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user