feat: update app icons and refactor home top professionals section to display dynamic agent profiles.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:real_estate_mobile/config/app_config.dart';
|
||||
import 'package:real_estate_mobile/core/network/api_client.dart';
|
||||
import 'package:real_estate_mobile/core/storage/secure_storage.dart';
|
||||
import 'package:real_estate_mobile/features/messaging/data/models/message.dart';
|
||||
import 'package:real_estate_mobile/features/support_chat/data/models/support_chat_models.dart';
|
||||
@@ -144,12 +145,31 @@ class SocketService {
|
||||
}
|
||||
|
||||
Future<void> _reconnectWithFreshToken() async {
|
||||
// Wait briefly for any in-flight REST token refresh to complete
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final token = await SecureStorage.getAccessToken();
|
||||
if (token != null && token != _currentToken) {
|
||||
if (token == null) return;
|
||||
|
||||
// If token hasn't changed, try triggering a refresh via a lightweight API call
|
||||
if (token == _currentToken) {
|
||||
try {
|
||||
// Make a lightweight call that triggers the API client's 401 interceptor
|
||||
// which auto-refreshes the token
|
||||
final dio = ApiClient.instance.dio;
|
||||
await dio.get('/auth/me');
|
||||
final refreshedToken = await SecureStorage.getAccessToken();
|
||||
if (refreshedToken == null || refreshedToken == _currentToken) return;
|
||||
_currentToken = refreshedToken;
|
||||
} catch (_) {
|
||||
return; // Token refresh failed — user will need to re-login
|
||||
}
|
||||
} else {
|
||||
_currentToken = token;
|
||||
_socket?.io.options?['auth'] = {'token': token};
|
||||
_socket?.connect();
|
||||
}
|
||||
|
||||
_socket?.io.options?['auth'] = {'token': _currentToken};
|
||||
_socket?.connect();
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
|
||||
@@ -97,7 +97,7 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
|
||||
final currentMessages =
|
||||
List<ChatMessage>.from(state.messages[convId] ?? []);
|
||||
|
||||
// Avoid duplicates
|
||||
// Avoid duplicates (broadcast may arrive after ack for sender's own message)
|
||||
if (currentMessages.any((m) => m.id == message.id)) return;
|
||||
|
||||
currentMessages.insert(0, message);
|
||||
@@ -105,12 +105,22 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
|
||||
Map<String, List<ChatMessage>>.from(state.messages)
|
||||
..[convId] = currentMessages;
|
||||
|
||||
// Show friendly preview for non-text messages (matching web)
|
||||
String previewText = message.content;
|
||||
if (message.messageType == MessageType.image) {
|
||||
previewText = 'Sent an image';
|
||||
} else if (message.messageType == MessageType.file) {
|
||||
previewText = 'Sent a file';
|
||||
} else if (_isGifUrl(message.content)) {
|
||||
previewText = 'GIF';
|
||||
}
|
||||
|
||||
// Update conversation's last message and unread count
|
||||
final updatedConversations = state.conversations.map((c) {
|
||||
if (c.id == convId) {
|
||||
final isActive = state.activeConversationId == convId;
|
||||
return c.copyWith(
|
||||
lastMessageText: message.content,
|
||||
lastMessageText: previewText,
|
||||
lastMessageAt: message.createdAt,
|
||||
unreadCount: isActive ? c.unreadCount : c.unreadCount + 1,
|
||||
);
|
||||
@@ -155,6 +165,59 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
|
||||
}).toList();
|
||||
state = state.copyWith(conversations: updatedConversations);
|
||||
}));
|
||||
|
||||
// Reconnection: re-join active conversation room and catch up missed messages
|
||||
_subscriptions.add(_socket.onConnectionChange.listen((connected) {
|
||||
if (!mounted || !connected) return;
|
||||
final activeConvId = state.activeConversationId;
|
||||
if (activeConvId != null) {
|
||||
_rejoinAndCatchUp(activeConvId);
|
||||
}
|
||||
// Refresh conversation list to catch any missed updates
|
||||
loadConversations();
|
||||
}));
|
||||
}
|
||||
|
||||
/// Re-join socket room and fetch missed messages after reconnection (matching web)
|
||||
Future<void> _rejoinAndCatchUp(String conversationId, {int attempt = 1}) async {
|
||||
try {
|
||||
await _socket.joinConversation(conversationId);
|
||||
} catch (_) {
|
||||
if (attempt < 3 && mounted && state.activeConversationId == conversationId) {
|
||||
await Future.delayed(Duration(seconds: attempt));
|
||||
return _rejoinAndCatchUp(conversationId, attempt: attempt + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch recent messages to catch any missed during disconnect
|
||||
if (!mounted || state.activeConversationId != conversationId) return;
|
||||
try {
|
||||
final result = await _repository.getMessages(conversationId, page: 1);
|
||||
final recentMessages = result.messages.reversed.toList();
|
||||
final currentMessages = List<ChatMessage>.from(state.messages[conversationId] ?? []);
|
||||
final existingIds = currentMessages.map((m) => m.id).toSet();
|
||||
final newMessages = recentMessages.where((m) => !existingIds.contains(m.id)).toList();
|
||||
|
||||
if (newMessages.isNotEmpty && mounted) {
|
||||
currentMessages.insertAll(0, newMessages);
|
||||
// Sort newest first
|
||||
currentMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
final updatedMessages = Map<String, List<ChatMessage>>.from(state.messages)
|
||||
..[conversationId] = currentMessages;
|
||||
state = state.copyWith(messages: updatedMessages);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.e('Failed to catch up messages after reconnect', error: e);
|
||||
}
|
||||
}
|
||||
|
||||
static bool _isGifUrl(String text) {
|
||||
final lower = text.trim().toLowerCase();
|
||||
return lower.endsWith('.gif') ||
|
||||
lower.contains('giphy.com/') ||
|
||||
lower.contains('giphy.gif') ||
|
||||
lower.contains('tenor.com/');
|
||||
}
|
||||
|
||||
/// Update online status for a user across all conversations (matching web)
|
||||
@@ -320,12 +383,22 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
|
||||
Map<String, List<ChatMessage>>.from(state.messages)
|
||||
..[conversationId] = messagesAfterSend;
|
||||
|
||||
// Update conversation's last message
|
||||
// Update conversation's last message with friendly preview
|
||||
final sent = realMessage;
|
||||
String sentPreview = sent.content;
|
||||
if (sent.messageType == MessageType.image) {
|
||||
sentPreview = 'Sent an image';
|
||||
} else if (sent.messageType == MessageType.file) {
|
||||
sentPreview = 'Sent a file';
|
||||
} else if (_isGifUrl(sent.content)) {
|
||||
sentPreview = 'GIF';
|
||||
}
|
||||
|
||||
final updatedConversations = state.conversations.map((c) {
|
||||
if (c.id == conversationId) {
|
||||
return c.copyWith(
|
||||
lastMessageText: realMessage!.content,
|
||||
lastMessageAt: realMessage.createdAt,
|
||||
lastMessageText: sentPreview,
|
||||
lastMessageAt: sent.createdAt,
|
||||
);
|
||||
}
|
||||
return c;
|
||||
|
||||
Reference in New Issue
Block a user