feat: enhance user avatar display with S3Image and initial-based fallback, improve socket reconnection logic, and update splash screen layout, assets, and Android permissions.

This commit is contained in:
pradeepkumar
2026-03-27 15:39:38 +05:30
parent ca4315b09d
commit 625db4bb99
7 changed files with 132 additions and 56 deletions

View File

@@ -1,4 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

View File

@@ -3,17 +3,21 @@
<!-- Gradient background from #c4d9d4 (top) to #f0f5fc (bottom) --> <!-- Gradient background from #c4d9d4 (top) to #f0f5fc (bottom) -->
<item android:drawable="@drawable/splash_gradient" /> <item android:drawable="@drawable/splash_gradient" />
<!-- House illustration centered --> <!-- House illustration — above center -->
<item> <item
android:gravity="center_horizontal"
android:top="0dp"
android:bottom="80dp">
<bitmap <bitmap
android:gravity="center" android:gravity="center"
android:src="@drawable/splash_house" /> android:src="@drawable/splash_house" />
</item> </item>
<!-- RE-Quest logo below center --> <!-- RE-Quest logo below center -->
<item <item
android:gravity="center_horizontal|bottom" android:gravity="center_horizontal"
android:bottom="200dp"> android:top="80dp"
android:bottom="0dp">
<bitmap <bitmap
android:gravity="center" android:gravity="center"
android:src="@drawable/splash_logo" /> android:src="@drawable/splash_logo" />

View File

@@ -3,17 +3,21 @@
<!-- Gradient background from #c4d9d4 (top) to #f0f5fc (bottom) --> <!-- Gradient background from #c4d9d4 (top) to #f0f5fc (bottom) -->
<item android:drawable="@drawable/splash_gradient" /> <item android:drawable="@drawable/splash_gradient" />
<!-- House illustration centered --> <!-- House illustration — above center -->
<item> <item
android:gravity="center_horizontal"
android:top="0dp"
android:bottom="80dp">
<bitmap <bitmap
android:gravity="center" android:gravity="center"
android:src="@drawable/splash_house" /> android:src="@drawable/splash_house" />
</item> </item>
<!-- RE-Quest logo below center --> <!-- RE-Quest logo below center -->
<item <item
android:gravity="center_horizontal|bottom" android:gravity="center_horizontal"
android:bottom="200dp"> android:top="80dp"
android:bottom="0dp">
<bitmap <bitmap
android:gravity="center" android:gravity="center"
android:src="@drawable/splash_logo" /> android:src="@drawable/splash_logo" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart';
import 'package:real_estate_mobile/core/widgets/s3_image.dart';
import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart';
import 'package:real_estate_mobile/features/notifications/presentation/providers/notification_provider.dart'; import 'package:real_estate_mobile/features/notifications/presentation/providers/notification_provider.dart';
@@ -195,22 +196,16 @@ class _MenuDrawer extends ConsumerWidget {
), ),
), ),
child: ClipOval( child: ClipOval(
child: user.avatar != null child: user.avatar != null &&
? Image.network( user.avatar!.isNotEmpty
user.avatar!, ? S3Image(
imageUrl: user.avatar!,
width: 70,
height: 70,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => errorWidget: (_) => _buildInitialAvatar(user.firstName),
const Icon(
Icons.person,
size: 36,
color: AppColors.hintText,
),
) )
: const Icon( : _buildInitialAvatar(user.firstName),
Icons.person,
size: 36,
color: AppColors.hintText,
),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@@ -488,6 +483,28 @@ class _MenuDrawer extends ConsumerWidget {
); );
} }
Widget _buildInitialAvatar(String? firstName) {
final initial = (firstName != null && firstName.isNotEmpty)
? firstName[0].toUpperCase()
: '?';
return Container(
width: 70,
height: 70,
color: AppColors.primaryDark.withValues(alpha: 0.1),
child: Center(
child: Text(
initial,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 28,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
),
);
}
Widget _buildMenuItem( Widget _buildMenuItem(
BuildContext context, { BuildContext context, {
required IconData icon, required IconData icon,

View File

@@ -20,6 +20,9 @@ class SocketService {
io.Socket? _socket; io.Socket? _socket;
bool _isConnected = false; bool _isConnected = false;
bool _intentionalDisconnect = false; bool _intentionalDisconnect = false;
bool _isReconnecting = false;
int _reconnectAttempts = 0;
static const _maxReconnectAttempts = 3;
String? _currentToken; String? _currentToken;
// Stream controllers for events // Stream controllers for events
@@ -81,6 +84,8 @@ class SocketService {
_socket!.onConnect((_) { _socket!.onConnect((_) {
_log.i('Socket connected: ${_socket!.id}'); _log.i('Socket connected: ${_socket!.id}');
_isConnected = true; _isConnected = true;
_reconnectAttempts = 0; // Reset on successful connect
_isReconnecting = false;
_connectionController.add(true); _connectionController.add(true);
}); });
@@ -89,9 +94,14 @@ class SocketService {
_isConnected = false; _isConnected = false;
_connectionController.add(false); _connectionController.add(false);
if (reason == 'io server disconnect' && !_intentionalDisconnect) { if (reason == 'io server disconnect' && !_intentionalDisconnect && !_isReconnecting) {
_log.w('Server rejected connection - token likely expired'); if (_reconnectAttempts < _maxReconnectAttempts) {
_reconnectAttempts++;
_log.w('Server rejected connection (attempt $_reconnectAttempts/$_maxReconnectAttempts) - refreshing token...');
_reconnectWithFreshToken(); _reconnectWithFreshToken();
} else {
_log.e('Max reconnect attempts reached - giving up');
}
} }
}); });
@@ -170,31 +180,52 @@ class SocketService {
} }
Future<void> _reconnectWithFreshToken() async { Future<void> _reconnectWithFreshToken() async {
// Wait briefly for any in-flight REST token refresh to complete _isReconnecting = true;
await Future.delayed(const Duration(milliseconds: 500)); // Wait with exponential backoff
final delay = Duration(seconds: _reconnectAttempts * 2);
_log.i('Waiting ${delay.inSeconds}s before reconnect attempt...');
await Future.delayed(delay);
final token = await SecureStorage.getAccessToken(); final token = await SecureStorage.getAccessToken();
if (token == null) return; if (token == null) {
_log.w('No access token available for socket reconnection');
return;
}
// If token hasn't changed, try triggering a refresh via a lightweight API call // If token hasn't changed, try triggering a refresh via a lightweight API call
if (token == _currentToken) { if (token == _currentToken) {
_log.i('Token unchanged, triggering refresh via /auth/me...');
try { try {
// Make a lightweight call that triggers the API client's 401 interceptor
// which auto-refreshes the token
final dio = ApiClient.instance.dio; final dio = ApiClient.instance.dio;
await dio.get('/auth/me'); await dio.get('/auth/me');
final refreshedToken = await SecureStorage.getAccessToken(); final refreshedToken = await SecureStorage.getAccessToken();
if (refreshedToken == null || refreshedToken == _currentToken) return; if (refreshedToken == null || refreshedToken == _currentToken) {
_currentToken = refreshedToken; _log.w('Token refresh did not produce a new token');
} catch (_) { // Still try reconnecting — token might be valid but socket had a glitch
return; // Token refresh failed — user will need to re-login
}
} else { } else {
_currentToken = token; _log.i('Got fresh token after refresh');
}
} catch (e) {
_log.e('Token refresh failed: $e');
// Still try reconnecting with existing token
}
} }
_socket?.io.options?['auth'] = {'token': _currentToken}; // Get the latest token (may have been refreshed)
_socket?.connect(); final freshToken = await SecureStorage.getAccessToken();
if (freshToken == null) return;
_log.i('Reconnecting socket with fresh token...');
// Full reconnect — dispose old socket and create new one
_socket?.disconnect();
_socket?.dispose();
_socket = null;
_isConnected = false;
_currentToken = null;
// Small delay before reconnecting
await Future.delayed(const Duration(milliseconds: 500));
await connect();
} }
/// Ensure socket is connected — reconnect if needed. /// Ensure socket is connected — reconnect if needed.
@@ -205,6 +236,8 @@ class SocketService {
void disconnect() { void disconnect() {
_intentionalDisconnect = true; _intentionalDisconnect = true;
_isReconnecting = false;
_reconnectAttempts = 0;
_socket?.disconnect(); _socket?.disconnect();
_socket?.dispose(); _socket?.dispose();
_socket = null; _socket = null;

View File

@@ -75,26 +75,41 @@ class _SplashScreenState extends State<SplashScreen>
), ),
child: FadeTransition( child: FadeTransition(
opacity: _fadeAnimation, opacity: _fadeAnimation,
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min,
children: [ children: [
Image.asset( SizedBox(
width: 150,
height: 108,
child: Image.asset(
'assets/images/splash_house.png', 'assets/images/splash_house.png',
width: 150, width: 150,
height: 108,
fit: BoxFit.contain, fit: BoxFit.contain,
semanticLabel: 'House illustration', semanticLabel: 'House illustration',
), ),
),
const SizedBox(height: 35), const SizedBox(height: 35),
Image.asset( SizedBox(
width: 264,
height: 55,
child: Image.asset(
'assets/images/splash_logo.png', 'assets/images/splash_logo.png',
width: 264, width: 264,
height: 55,
fit: BoxFit.contain, fit: BoxFit.contain,
semanticLabel: 'RE-Quest logo', semanticLabel: 'RE-Quest logo',
), ),
),
], ],
), ),
), ),
), ),
),
),
); );
} }
} }