refactor: improvements
This commit is contained in:
@@ -20,7 +20,7 @@ class AuthState {
|
|||||||
|
|
||||||
final AuthStatus status;
|
final AuthStatus status;
|
||||||
final String? token;
|
final String? token;
|
||||||
final dynamic user; // Replace with proper User type
|
final User? user;
|
||||||
final String? error;
|
final String? error;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ class AuthState {
|
|||||||
AuthState copyWith({
|
AuthState copyWith({
|
||||||
AuthStatus? status,
|
AuthStatus? status,
|
||||||
String? token,
|
String? token,
|
||||||
dynamic user,
|
User? user,
|
||||||
String? error,
|
String? error,
|
||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
bool clearToken = false,
|
bool clearToken = false,
|
||||||
@@ -460,6 +460,7 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
// Clear token from storage
|
// Clear token from storage
|
||||||
final storage = ref.read(optimizedStorageServiceProvider);
|
final storage = ref.read(optimizedStorageServiceProvider);
|
||||||
await storage.deleteAuthToken();
|
await storage.deleteAuthToken();
|
||||||
|
_updateApiServiceToken(null);
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
@@ -497,6 +498,7 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
// Clear all local auth data
|
// Clear all local auth data
|
||||||
final storage = ref.read(optimizedStorageServiceProvider);
|
final storage = ref.read(optimizedStorageServiceProvider);
|
||||||
await storage.clearAuthData();
|
await storage.clearAuthData();
|
||||||
|
_updateApiServiceToken(null);
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
@@ -518,6 +520,7 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
clearUser: true,
|
clearUser: true,
|
||||||
error: 'Logout error: $e',
|
error: 'Logout error: $e',
|
||||||
);
|
);
|
||||||
|
_updateApiServiceToken(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,8 +531,11 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
if (state.token != null) {
|
if (state.token != null) {
|
||||||
final jwtUserInfo = TokenValidator.extractUserInfo(state.token!);
|
final jwtUserInfo = TokenValidator.extractUserInfo(state.token!);
|
||||||
if (jwtUserInfo != null) {
|
if (jwtUserInfo != null) {
|
||||||
DebugLogger.auth('Extracted user info from JWT token');
|
final userFromJwt = _userFromJwtClaims(jwtUserInfo);
|
||||||
state = state.copyWith(user: jwtUserInfo);
|
if (userFromJwt != null) {
|
||||||
|
DebugLogger.auth('Extracted user info from JWT token');
|
||||||
|
state = state.copyWith(user: userFromJwt);
|
||||||
|
}
|
||||||
|
|
||||||
// Still try to load from server in background for complete data
|
// Still try to load from server in background for complete data
|
||||||
Future.microtask(() => _loadServerUserData());
|
Future.microtask(() => _loadServerUserData());
|
||||||
@@ -569,7 +575,7 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update API service with current token
|
/// Update API service with current token
|
||||||
void _updateApiServiceToken(String token) {
|
void _updateApiServiceToken(String? token) {
|
||||||
final api = ref.read(apiServiceProvider);
|
final api = ref.read(apiServiceProvider);
|
||||||
api?.updateAuthToken(token);
|
api?.updateAuthToken(token);
|
||||||
}
|
}
|
||||||
@@ -689,6 +695,46 @@ class AuthStateManager extends Notifier<AuthState> {
|
|||||||
'storageCache': 'Managed by OptimizedStorageService',
|
'storageCache': 'Managed by OptimizedStorageService',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
User? _userFromJwtClaims(Map<String, dynamic> claims) {
|
||||||
|
final id =
|
||||||
|
(claims['sub'] ?? claims['username'] ?? claims['email'])
|
||||||
|
?.toString()
|
||||||
|
.trim() ??
|
||||||
|
'';
|
||||||
|
final username =
|
||||||
|
(claims['username'] ?? claims['name'])?.toString().trim() ?? '';
|
||||||
|
final emailValue = claims['email'];
|
||||||
|
final email = emailValue == null ? '' : emailValue.toString().trim();
|
||||||
|
|
||||||
|
if (id.isEmpty && username.isEmpty && email.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String resolvedRole = 'user';
|
||||||
|
final roles = claims['roles'];
|
||||||
|
if (roles is List && roles.isNotEmpty) {
|
||||||
|
resolvedRole = roles.first.toString();
|
||||||
|
} else if (roles is String && roles.isNotEmpty) {
|
||||||
|
resolvedRole = roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
return User(
|
||||||
|
id: id.isNotEmpty
|
||||||
|
? id
|
||||||
|
: (username.isNotEmpty ? username : email.ifEmptyReturn('user')),
|
||||||
|
username: username.ifEmptyReturn(
|
||||||
|
email.ifEmptyReturn(id.ifEmptyReturn('user')),
|
||||||
|
),
|
||||||
|
email: email,
|
||||||
|
role: resolvedRole,
|
||||||
|
isActive: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _StringFallbackExtension on String {
|
||||||
|
String ifEmptyReturn(String fallback) => isEmpty ? fallback : this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provider for the unified auth state manager
|
/// Provider for the unified auth state manager
|
||||||
@@ -707,7 +753,7 @@ final authTokenProvider2 = Provider<String?>((ref) {
|
|||||||
return ref.watch(authStateManagerProvider.select((state) => state.token));
|
return ref.watch(authStateManagerProvider.select((state) => state.token));
|
||||||
});
|
});
|
||||||
|
|
||||||
final authUserProvider = Provider<dynamic>((ref) {
|
final authUserProvider = Provider<User?>((ref) {
|
||||||
return ref.watch(authStateManagerProvider.select((state) => state.user));
|
return ref.watch(authStateManagerProvider.select((state) => state.user));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -249,10 +249,9 @@ final apiTokenUpdaterProvider = Provider<void>((ref) {
|
|||||||
ref.listen<String?>(authTokenProvider3, (previous, next) {
|
ref.listen<String?>(authTokenProvider3, (previous, next) {
|
||||||
final api = ref.read(apiServiceProvider);
|
final api = ref.read(apiServiceProvider);
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
api.updateAuthToken(next ?? '');
|
api.updateAuthToken(next);
|
||||||
foundation.debugPrint(
|
final length = next?.length ?? 0;
|
||||||
'DEBUG: Applied auth token to API (len=${next?.length ?? 0})',
|
foundation.debugPrint('DEBUG: Applied auth token to API (len=$length)');
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -160,8 +160,13 @@ final appStartupFlowProvider = Provider<void>((ref) {
|
|||||||
// match theme after the first frame. Avoids flicker at startup.
|
// match theme after the first frame. Avoids flicker at startup.
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
try {
|
try {
|
||||||
final isDark =
|
final context = NavigationService.context;
|
||||||
WidgetsBinding.instance.window.platformBrightness == Brightness.dark;
|
final view = context != null ? View.maybeOf(context) : null;
|
||||||
|
final dispatcher = WidgetsBinding.instance.platformDispatcher;
|
||||||
|
final platformBrightness =
|
||||||
|
view?.platformDispatcher.platformBrightness ??
|
||||||
|
dispatcher.platformBrightness;
|
||||||
|
final isDark = platformBrightness == Brightness.dark;
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
SystemUiOverlayStyle(
|
SystemUiOverlayStyle(
|
||||||
statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
|
statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class ApiService {
|
|||||||
// Validation interceptor fully removed
|
// Validation interceptor fully removed
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateAuthToken(String token) {
|
void updateAuthToken(String? token) {
|
||||||
_authInterceptor.updateAuthToken(token);
|
_authInterceptor.updateAuthToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,29 +317,15 @@ class ApiService {
|
|||||||
allRegularChats = regularResponse.data as List;
|
allRegularChats = regularResponse.data as List;
|
||||||
}
|
}
|
||||||
|
|
||||||
final pinnedResponse = await _dio.get('/api/v1/chats/pinned');
|
final pinnedChatList = await _fetchChatCollection(
|
||||||
final archivedResponse = await _dio.get('/api/v1/chats/all/archived');
|
'/api/v1/chats/pinned',
|
||||||
|
debugLabel: 'pinned chats',
|
||||||
debugPrint('DEBUG: Pinned response status: ${pinnedResponse.statusCode}');
|
);
|
||||||
debugPrint(
|
final archivedChatList = await _fetchChatCollection(
|
||||||
'DEBUG: Archived response status: ${archivedResponse.statusCode}',
|
'/api/v1/chats/all/archived',
|
||||||
|
debugLabel: 'archived chats',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pinnedResponse.data is! List) {
|
|
||||||
throw Exception(
|
|
||||||
'Expected array of pinned chats, got ${pinnedResponse.data.runtimeType}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (archivedResponse.data is! List) {
|
|
||||||
throw Exception(
|
|
||||||
'Expected array of archived chats, got ${archivedResponse.data.runtimeType}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final regularChatList = allRegularChats;
|
final regularChatList = allRegularChats;
|
||||||
final pinnedChatList = pinnedResponse.data as List;
|
|
||||||
final archivedChatList = archivedResponse.data as List;
|
|
||||||
|
|
||||||
debugPrint('DEBUG: Found ${regularChatList.length} regular chats');
|
debugPrint('DEBUG: Found ${regularChatList.length} regular chats');
|
||||||
debugPrint('DEBUG: Found ${pinnedChatList.length} pinned chats');
|
debugPrint('DEBUG: Found ${pinnedChatList.length} pinned chats');
|
||||||
@@ -377,6 +363,7 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process regular conversations (excluding pinned and archived ones)
|
// Process regular conversations (excluding pinned and archived ones)
|
||||||
|
var loggedSampleChat = false;
|
||||||
for (final chatData in regularChatList) {
|
for (final chatData in regularChatList) {
|
||||||
try {
|
try {
|
||||||
// Debug: Check if conversation has folder_id in raw data
|
// Debug: Check if conversation has folder_id in raw data
|
||||||
@@ -387,8 +374,8 @@ class ApiService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug: Check what fields are available in the chat data
|
if (!loggedSampleChat) {
|
||||||
if (regularChatList.indexOf(chatData) == 0) {
|
loggedSampleChat = true;
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'🔍 DEBUG: Sample chat data fields: ${chatData.keys.toList()}',
|
'🔍 DEBUG: Sample chat data fields: ${chatData.keys.toList()}',
|
||||||
);
|
);
|
||||||
@@ -417,6 +404,29 @@ class ApiService {
|
|||||||
return conversations;
|
return conversations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<dynamic>> _fetchChatCollection(
|
||||||
|
String path, {
|
||||||
|
required String debugLabel,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.get(path);
|
||||||
|
DebugLogger.log('$debugLabel response status: ${response.statusCode}');
|
||||||
|
if (response.data is List) {
|
||||||
|
return (response.data as List).cast<dynamic>();
|
||||||
|
}
|
||||||
|
DebugLogger.warning(
|
||||||
|
'Expected array for $debugLabel, got ${response.data.runtimeType}',
|
||||||
|
);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
DebugLogger.warning(
|
||||||
|
'Skipping $debugLabel due to network error: ${e.message}',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
DebugLogger.warning('Skipping $debugLabel due to error: $e');
|
||||||
|
}
|
||||||
|
return <dynamic>[];
|
||||||
|
}
|
||||||
|
|
||||||
// Helper method to safely parse timestamps
|
// Helper method to safely parse timestamps
|
||||||
DateTime _parseTimestamp(dynamic timestamp) {
|
DateTime _parseTimestamp(dynamic timestamp) {
|
||||||
if (timestamp == null) return DateTime.now();
|
if (timestamp == null) return DateTime.now();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import '../providers/app_providers.dart';
|
import '../providers/app_providers.dart';
|
||||||
@@ -8,6 +7,7 @@ enum ConnectivityStatus { online, offline, checking }
|
|||||||
|
|
||||||
class ConnectivityService {
|
class ConnectivityService {
|
||||||
final Dio _dio;
|
final Dio _dio;
|
||||||
|
final Ref _ref;
|
||||||
Timer? _connectivityTimer;
|
Timer? _connectivityTimer;
|
||||||
final _connectivityController =
|
final _connectivityController =
|
||||||
StreamController<ConnectivityStatus>.broadcast();
|
StreamController<ConnectivityStatus>.broadcast();
|
||||||
@@ -16,7 +16,7 @@ class ConnectivityService {
|
|||||||
Duration _interval = const Duration(seconds: 10);
|
Duration _interval = const Duration(seconds: 10);
|
||||||
int _lastLatencyMs = -1;
|
int _lastLatencyMs = -1;
|
||||||
|
|
||||||
ConnectivityService(this._dio) {
|
ConnectivityService(this._dio, this._ref) {
|
||||||
_startConnectivityMonitoring();
|
_startConnectivityMonitoring();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,41 +45,31 @@ class ConnectivityService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _checkConnectivity() async {
|
Future<void> _checkConnectivity() async {
|
||||||
try {
|
final serverReachability = await _probeActiveServer();
|
||||||
// DNS lookup is a lightweight, permission-free reachability check
|
if (serverReachability != null) {
|
||||||
final result = await InternetAddress.lookup(
|
if (serverReachability) {
|
||||||
'google.com',
|
|
||||||
).timeout(const Duration(seconds: 2));
|
|
||||||
|
|
||||||
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
|
|
||||||
_updateStatus(ConnectivityStatus.online);
|
_updateStatus(ConnectivityStatus.online);
|
||||||
return;
|
} else {
|
||||||
|
_lastLatencyMs = -1;
|
||||||
|
_updateStatus(ConnectivityStatus.offline);
|
||||||
}
|
}
|
||||||
} catch (_) {
|
return;
|
||||||
// Swallow and continue to HTTP reachability check
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// As a secondary check, hit a public 204 endpoint that returns quickly
|
final fallbackReachability = await _probeAnyKnownServer();
|
||||||
try {
|
if (fallbackReachability != null) {
|
||||||
final start = DateTime.now();
|
if (fallbackReachability) {
|
||||||
await _dio
|
_updateStatus(ConnectivityStatus.online);
|
||||||
.get(
|
} else {
|
||||||
'https://www.google.com/generate_204',
|
_lastLatencyMs = -1;
|
||||||
options: Options(
|
_updateStatus(ConnectivityStatus.offline);
|
||||||
method: 'GET',
|
}
|
||||||
sendTimeout: const Duration(seconds: 2),
|
return;
|
||||||
receiveTimeout: const Duration(seconds: 2),
|
|
||||||
followRedirects: false,
|
|
||||||
validateStatus: (status) => status != null && status < 400,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.timeout(const Duration(seconds: 2));
|
|
||||||
_lastLatencyMs = DateTime.now().difference(start).inMilliseconds;
|
|
||||||
_updateStatus(ConnectivityStatus.online);
|
|
||||||
} catch (_) {
|
|
||||||
_lastLatencyMs = -1;
|
|
||||||
_updateStatus(ConnectivityStatus.offline);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No configured server to probe; assume usable connectivity so setup flows continue.
|
||||||
|
_lastLatencyMs = -1;
|
||||||
|
_updateStatus(ConnectivityStatus.online);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateStatus(ConnectivityStatus status) {
|
void _updateStatus(ConnectivityStatus status) {
|
||||||
@@ -120,13 +110,103 @@ class ConnectivityService {
|
|||||||
_connectivityTimer?.cancel();
|
_connectivityTimer?.cancel();
|
||||||
_connectivityController.close();
|
_connectivityController.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool?> _probeActiveServer() async {
|
||||||
|
final healthUri = _resolveHealthUri();
|
||||||
|
if (healthUri == null) return null;
|
||||||
|
|
||||||
|
return _probeHealthEndpoint(healthUri, updateLatency: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool?> _probeAnyKnownServer() async {
|
||||||
|
try {
|
||||||
|
final configs = await _ref.read(serverConfigsProvider.future);
|
||||||
|
for (final config in configs) {
|
||||||
|
final uri = _buildHealthUri(config.url);
|
||||||
|
if (uri == null) continue;
|
||||||
|
final result = await _probeHealthEndpoint(uri);
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool?> _probeHealthEndpoint(
|
||||||
|
Uri uri, {
|
||||||
|
bool updateLatency = false,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final start = DateTime.now();
|
||||||
|
final response = await _dio
|
||||||
|
.getUri(
|
||||||
|
uri,
|
||||||
|
options: Options(
|
||||||
|
method: 'GET',
|
||||||
|
sendTimeout: const Duration(seconds: 3),
|
||||||
|
receiveTimeout: const Duration(seconds: 3),
|
||||||
|
followRedirects: false,
|
||||||
|
validateStatus: (status) => status != null && status < 500,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.timeout(const Duration(seconds: 4));
|
||||||
|
|
||||||
|
final isHealthy =
|
||||||
|
response.statusCode == 200 && _responseIndicatesHealth(response.data);
|
||||||
|
if (isHealthy && updateLatency) {
|
||||||
|
_lastLatencyMs = DateTime.now().difference(start).inMilliseconds;
|
||||||
|
}
|
||||||
|
return isHealthy;
|
||||||
|
} catch (_) {
|
||||||
|
// Treat as unreachable.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri? _resolveHealthUri() {
|
||||||
|
final api = _ref.read(apiServiceProvider);
|
||||||
|
if (api != null) {
|
||||||
|
return _buildHealthUri(api.baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
final activeServer = _ref.read(activeServerProvider);
|
||||||
|
return activeServer.maybeWhen(
|
||||||
|
data: (server) => server != null ? _buildHealthUri(server.url) : null,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri? _buildHealthUri(String baseUrl) {
|
||||||
|
if (baseUrl.isEmpty) return null;
|
||||||
|
|
||||||
|
Uri? parsed = Uri.tryParse(baseUrl.trim());
|
||||||
|
if (parsed == null) return null;
|
||||||
|
|
||||||
|
if (!parsed.hasScheme) {
|
||||||
|
parsed =
|
||||||
|
Uri.tryParse('https://$baseUrl') ?? Uri.tryParse('http://$baseUrl');
|
||||||
|
}
|
||||||
|
if (parsed == null) return null;
|
||||||
|
|
||||||
|
return parsed.resolve('health');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _responseIndicatesHealth(dynamic data) {
|
||||||
|
if (data is Map) {
|
||||||
|
final dynamic status = data['status'];
|
||||||
|
if (status is bool) return status;
|
||||||
|
if (status is num) return status != 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Providers
|
// Providers
|
||||||
final connectivityServiceProvider = Provider<ConnectivityService>((ref) {
|
final connectivityServiceProvider = Provider<ConnectivityService>((ref) {
|
||||||
// Use a lightweight Dio instance only for connectivity checks
|
// Use a lightweight Dio instance only for connectivity checks
|
||||||
final dio = Dio();
|
final dio = Dio();
|
||||||
final service = ConnectivityService(dio);
|
final service = ConnectivityService(dio, ref);
|
||||||
ref.onDispose(() => service.dispose());
|
ref.onDispose(() => service.dispose());
|
||||||
return service;
|
return service;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
/// Applies a single System UI overlay style after first frame to avoid flicker
|
/// Applies a single System UI overlay style after first frame to avoid flicker
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../../core/auth/auth_state_manager.dart';
|
import '../../../core/auth/auth_state_manager.dart';
|
||||||
|
import '../../../core/models/user.dart';
|
||||||
import '../../../core/providers/app_providers.dart';
|
import '../../../core/providers/app_providers.dart';
|
||||||
|
|
||||||
/// Unified auth providers using the new auth state manager
|
/// Unified auth providers using the new auth state manager
|
||||||
@@ -75,7 +76,7 @@ final authTokenProvider3 = Provider<String?>((ref) {
|
|||||||
return ref.watch(authStateManagerProvider.select((state) => state.token));
|
return ref.watch(authStateManagerProvider.select((state) => state.token));
|
||||||
});
|
});
|
||||||
|
|
||||||
final currentUserProvider2 = Provider<dynamic>((ref) {
|
final currentUserProvider2 = Provider<User?>((ref) {
|
||||||
return ref.watch(authStateManagerProvider.select((state) => state.user));
|
return ref.watch(authStateManagerProvider.select((state) => state.user));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -632,7 +632,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
Spacing.lg,
|
Spacing.lg,
|
||||||
),
|
),
|
||||||
physics:
|
physics:
|
||||||
const NeverScrollableScrollPhysics(), // Prevent scrolling during load
|
const AlwaysScrollableScrollPhysics(), // Allow pull-to-refresh while loading
|
||||||
// Modest cache extent to avoid offscreen overwork but keep shimmer smooth
|
// Modest cache extent to avoid offscreen overwork but keep shimmer smooth
|
||||||
cacheExtent: 300,
|
cacheExtent: 300,
|
||||||
itemCount: 6,
|
itemCount: 6,
|
||||||
@@ -708,6 +708,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
return OptimizedList<ChatMessage>(
|
return OptimizedList<ChatMessage>(
|
||||||
key: const ValueKey('actual_messages'),
|
key: const ValueKey('actual_messages'),
|
||||||
scrollController: _scrollController,
|
scrollController: _scrollController,
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
items: messages,
|
items: messages,
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
Spacing.lg,
|
Spacing.lg,
|
||||||
@@ -868,58 +869,68 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
data: (user) => user,
|
data: (user) => user,
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
final dynamic authUser = ref.watch(authUserProvider);
|
final authUser = ref.watch(authUserProvider);
|
||||||
final user = userFromProfile ?? authUser;
|
final user = userFromProfile ?? authUser;
|
||||||
final greetingName = deriveUserDisplayName(user);
|
final greetingName = deriveUserDisplayName(user);
|
||||||
return Center(
|
return LayoutBuilder(
|
||||||
child: SingleChildScrollView(
|
builder: (context, constraints) {
|
||||||
padding: const EdgeInsets.all(Spacing.lg),
|
return SingleChildScrollView(
|
||||||
child: Column(
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
padding: const EdgeInsets.all(Spacing.lg),
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
child: ConstrainedBox(
|
||||||
children: [
|
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||||
// Minimal, clean empty state
|
child: Column(
|
||||||
Container(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
width: Spacing.xxl + Spacing.xxxl,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
height: Spacing.xxl + Spacing.xxxl,
|
children: [
|
||||||
decoration: BoxDecoration(
|
// Minimal, clean empty state
|
||||||
gradient: LinearGradient(
|
Container(
|
||||||
begin: Alignment.topLeft,
|
width: Spacing.xxl + Spacing.xxxl,
|
||||||
end: Alignment.bottomRight,
|
height: Spacing.xxl + Spacing.xxxl,
|
||||||
colors: [
|
decoration: BoxDecoration(
|
||||||
context.conduitTheme.buttonPrimary,
|
gradient: LinearGradient(
|
||||||
context.conduitTheme.buttonPrimary.withValues(
|
begin: Alignment.topLeft,
|
||||||
alpha: 0.8,
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
context.conduitTheme.buttonPrimary,
|
||||||
|
context.conduitTheme.buttonPrimary.withValues(
|
||||||
|
alpha: 0.8,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
borderRadius: BorderRadius.circular(
|
||||||
),
|
AppBorderRadius.round,
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.round),
|
),
|
||||||
boxShadow: ConduitShadows.glow,
|
boxShadow: ConduitShadows.glow,
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Platform.isIOS ? CupertinoIcons.chat_bubble_2 : Icons.chat,
|
Platform.isIOS
|
||||||
size: Spacing.xxxl - Spacing.xs,
|
? CupertinoIcons.chat_bubble_2
|
||||||
color: context.conduitTheme.textInverse,
|
: Icons.chat,
|
||||||
),
|
size: Spacing.xxxl - Spacing.xs,
|
||||||
)
|
color: context.conduitTheme.textInverse,
|
||||||
.animate()
|
),
|
||||||
.scale(duration: const Duration(milliseconds: 300))
|
)
|
||||||
.then()
|
.animate()
|
||||||
.shimmer(duration: const Duration(milliseconds: 1200)),
|
.scale(duration: const Duration(milliseconds: 300))
|
||||||
|
.then()
|
||||||
|
.shimmer(duration: const Duration(milliseconds: 1200)),
|
||||||
|
|
||||||
const SizedBox(height: Spacing.xl),
|
const SizedBox(height: Spacing.xl),
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
l10n.onboardStartTitle(greetingName),
|
l10n.onboardStartTitle(greetingName),
|
||||||
style: theme.textTheme.headlineSmall?.copyWith(
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: context.conduitTheme.textPrimary,
|
color: context.conduitTheme.textPrimary,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
).animate().fadeIn(delay: const Duration(milliseconds: 150)),
|
).animate().fadeIn(delay: const Duration(milliseconds: 150)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1218,7 +1218,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
data: (u) => u,
|
data: (u) => u,
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
final dynamic authUser = ref.watch(authUserProvider);
|
final authUser = ref.watch(authUserProvider);
|
||||||
final user = userFromProfile ?? authUser;
|
final user = userFromProfile ?? authUser;
|
||||||
final api = ref.watch(apiServiceProvider);
|
final api = ref.watch(apiServiceProvider);
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class _OnboardingSheetState extends ConsumerState<OnboardingSheet> {
|
|||||||
data: (user) => user,
|
data: (user) => user,
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
final dynamic authUser = ref.watch(authUserProvider);
|
final authUser = ref.watch(authUserProvider);
|
||||||
final user = userFromProfile ?? authUser;
|
final user = userFromProfile ?? authUser;
|
||||||
final greetingName = deriveUserDisplayName(user);
|
final greetingName = deriveUserDisplayName(user);
|
||||||
final pages = _buildPages(l10n, greetingName);
|
final pages = _buildPages(l10n, greetingName);
|
||||||
|
|||||||
@@ -63,7 +63,19 @@ class TaskQueueNotifier extends Notifier<List<OutboundTask>> {
|
|||||||
Future<void> _save() async {
|
Future<void> _save() async {
|
||||||
try {
|
try {
|
||||||
final prefs = ref.read(sharedPreferencesProvider);
|
final prefs = ref.read(sharedPreferencesProvider);
|
||||||
final raw = state.map((t) => t.toJson()).toList(growable: false);
|
final retained = [
|
||||||
|
for (final task in state)
|
||||||
|
if (task.status == TaskStatus.queued ||
|
||||||
|
task.status == TaskStatus.running)
|
||||||
|
task,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (retained.length != state.length) {
|
||||||
|
// Remove completed entries from state to keep the in-memory queue lean.
|
||||||
|
state = retained;
|
||||||
|
}
|
||||||
|
|
||||||
|
final raw = retained.map((t) => t.toJson()).toList(growable: false);
|
||||||
await prefs.setString(_prefsKey, jsonEncode(raw));
|
await prefs.setString(_prefsKey, jsonEncode(raw));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('DEBUG: Failed to persist task queue: $e');
|
debugPrint('DEBUG: Failed to persist task queue: $e');
|
||||||
|
|||||||
@@ -51,15 +51,14 @@ class _ImprovedLoadingStateState extends State<ImprovedLoadingState>
|
|||||||
);
|
);
|
||||||
_animationController.forward();
|
_animationController.forward();
|
||||||
|
|
||||||
// Announce loading state for screen readers
|
// Announce loading state for screen readers using localized messaging.
|
||||||
if (widget.message != null) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
if (!mounted) return;
|
||||||
SemanticsService.announce(
|
final l10n = AppLocalizations.of(context);
|
||||||
'Loading: ${widget.message}',
|
final announcement = widget.message ?? l10n?.loadingContent ?? 'Loading';
|
||||||
TextDirection.ltr,
|
final direction = Directionality.maybeOf(context) ?? TextDirection.ltr;
|
||||||
);
|
SemanticsService.announce(announcement, direction);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class _WasOfflineNotifier extends Notifier<bool> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
loading: () {},
|
loading: () {},
|
||||||
error: (_, __) {},
|
error: (error, stackTrace) {},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
Reference in New Issue
Block a user