Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
446 changes: 446 additions & 0 deletions mobile/lib/controllers/chat/active_chat_controller.dart

Large diffs are not rendered by default.

155 changes: 155 additions & 0 deletions mobile/lib/controllers/chat/chat_connection_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:mobile/services/chat/chat_event_handler.dart';
import 'package:mobile/services/chat/chat_sync_service.dart';
import '../../services/ws_service.dart';
import '../../services/auth_service.dart';

class ChatConnectionController extends ChangeNotifier
with WidgetsBindingObserver {
final WebSocketService _ws = WebSocketService();
final AuthService _auth = AuthService();
final ChatSyncService _syncService = ChatSyncService();
final ChatEventHandler eventHandler;

bool isOffline = false;
bool _isWsConnecting = false;
Timer? _reconnectTimer;
StreamSubscription? _connectivitySubscription;
StreamSubscription? _wsSubscription;

ChatConnectionController({required this.eventHandler}) {
WidgetsBinding.instance.addObserver(this);

_connectivitySubscription = Connectivity().onConnectivityChanged.listen((
result,
) {
final bool currentlyOffline = result.contains(ConnectivityResult.none);

if (isOffline != currentlyOffline) {
isOffline = currentlyOffline;
notifyListeners();

if (!isOffline) {
connectWebSocket();
eventHandler.inboxController
.loadInbox();
} else {
eventHandler.activeChatController.isPeerOnline = false;
eventHandler.activeChatController.isPeerTyping = false;
eventHandler.activeChatController.refreshUI();

_ws.disconnect();
}
}
});
}

Future<void> connectWebSocket() async {
if (_ws.isConnected || _isWsConnecting) return;
_isWsConnecting = true;

try {
_reconnectTimer?.cancel();

final token = await _auth.getToken();
if (token == null) {
_isWsConnecting = false;
return;
}

await _wsSubscription?.cancel();
_ws.disconnect();

final connected = await _ws.connect(token);
if (!connected) {
_triggerReconnectLoop();
return;
}

_syncService.processOfflineQueue(eventHandler.currentUserId);

final currentChatId = eventHandler.activeChatController.currentChatUserId;
if (currentChatId != null &&
!eventHandler.activeChatController.isCurrentChatGroup) {
_ws.sendRequestStatus(targetId: currentChatId);
}

_wsSubscription = _ws.stream?.listen(
(rawFrame) {
try {
debugPrint("📥 WS Received: $rawFrame");
final decoded = jsonDecode(rawFrame);
if (decoded is Map<String, dynamic>) {
eventHandler.handleIncomingEvent(decoded);
}
} catch (e) {
debugPrint("WebSocket payload error: $e");
}
},
onError: (err) {
debugPrint("WS Pipeline Error: $err");
_triggerReconnectLoop();
},
onDone: () {
debugPrint("WS Pipeline Closed by Server.");
_triggerReconnectLoop();
},
);
} catch (e) {
debugPrint("WS Setup Error: $e");
_triggerReconnectLoop();
} finally {
_isWsConnecting = false;
}
}

void _triggerReconnectLoop() {
_ws.disconnect();
_isWsConnecting = false;

if (eventHandler.activeChatController.isPeerOnline) {
eventHandler.activeChatController.isPeerOnline = false;
eventHandler.activeChatController.refreshUI();
}

_reconnectTimer?.cancel();
_reconnectTimer = Timer(const Duration(seconds: 4), () {
if (!isOffline) {
connectWebSocket();
}
});
}

void disconnectWebSocket() {
_reconnectTimer?.cancel();
_ws.disconnect();
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
connectWebSocket();
_syncService.processOfflineQueue(
eventHandler.currentUserId,
);
eventHandler.inboxController
.loadInbox();
} else if (state == AppLifecycleState.paused) {
_reconnectTimer?.cancel();
_ws.disconnect();
}
}

@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_connectivitySubscription?.cancel();
_wsSubscription?.cancel();
_reconnectTimer?.cancel();
_ws.disconnect();
super.dispose();
}
}
52 changes: 52 additions & 0 deletions mobile/lib/controllers/chat/chat_search_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import '../../services/api_services.dart';

class ChatSearchController extends ChangeNotifier {
final ApiService _api = ApiService();

List<dynamic> contactSearchResults = [];
bool isSearchLoading = false;

Future<void> queryUsers(String term) async {
final String cleanTerm = term.trim();
if (cleanTerm.isEmpty || cleanTerm.length < 3) {
contactSearchResults.clear();
notifyListeners();
return;
}

isSearchLoading = true;
notifyListeners();

try {
final res = await _api.searchUsers(term);
contactSearchResults = _extractDataList(res.data, ['users']);
} catch (e) {
debugPrint("User query failure: $e");
contactSearchResults.clear();
} finally {
isSearchLoading = false;
notifyListeners();
}
}

void clearSearch() {
if (contactSearchResults.isNotEmpty || isSearchLoading) {
contactSearchResults.clear();
isSearchLoading = false;
notifyListeners();
}
}

List<dynamic> _extractDataList(dynamic data, List<String> fallbackKeys) {
if (data == null) return [];
if (data is List) return data;
if (data is Map) {
if (data['data'] is List) return data['data'];
for (final key in fallbackKeys) {
if (data[key] is List) return data[key];
}
}
return [];
}
}
92 changes: 92 additions & 0 deletions mobile/lib/controllers/chat/group_details_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:mobile/pages/chat/chat_details_page.dart';
import '../../services/api_services.dart';

class GroupDetailsController extends ChangeNotifier {
final ApiService _api = ApiService();

List<ChatMember> currentGroupMembers = [];
Map<String, String> groupMemberNames = {};
Map<String, String> userCache = {};
bool isLoadingDetails = false;
final Set<String> _fetchedGroups = {};
bool hasFetchedGroup(String groupId) => _fetchedGroups.contains(groupId);

Future<void> fetchGroupMembers(String groupId) async {
try {
isLoadingDetails = true;
notifyListeners();

final response = await _api.getGroupMembers(groupId);
if (response.statusCode == 200) {
final List<dynamic> memberList = _extractDataList(response.data, [
'members',
'data',
]);

currentGroupMembers = memberList
.map((json) => ChatMember.fromJson(json))
.toList();

for (var member in memberList) {
final uid = member['user_id'].toString();
userCache[uid] = member['display_name'] ?? 'Member';
groupMemberNames[uid] = member['display_name'] ?? 'Unknown';
}
}
} catch (e) {
debugPrint("Error fetching members: $e");
} finally {
isLoadingDetails = false;
notifyListeners();
}
}

Future<void> preloadGroupMembers(String groupId) async {
if (_fetchedGroups.contains(groupId)) return;

_fetchedGroups.add(groupId);

try {
final res = await _api.getGroupMembers(groupId);
final members = _extractDataList(res.data, ['members', 'data']);

bool updatedCache = false;
for (var m in members) {
final uid = m['user_id'].toString();
final name = m['display_name'] ?? 'Member';

if (userCache[uid] != name) {
userCache[uid] = name;
updatedCache = true;
}
}

if (updatedCache) notifyListeners();
} catch (e) {
_fetchedGroups.remove(groupId);
debugPrint("Failed to preload group members for $groupId: $e");
}
}

void clearCache() {
currentGroupMembers.clear();
groupMemberNames.clear();
userCache.clear();
_fetchedGroups.clear();
isLoadingDetails = false;
notifyListeners();
}

List<dynamic> _extractDataList(dynamic data, List<String> fallbackKeys) {
if (data == null) return [];
if (data is List) return data;
if (data is Map) {
if (data['data'] is List) return data['data'];
for (final key in fallbackKeys) {
if (data[key] is List) return data[key];
}
}
return [];
}
}
Loading
Loading