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
146 changes: 81 additions & 65 deletions mobile/lib/controllers/chat.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,29 @@ class ChatController extends ChangeNotifier {
List<Conversation> inbox = [];
List<Message> activeChat = [];
List<dynamic> contactSearchResults = [];

String? currentChatUserId;
bool isPeerTyping = false;
bool isPeerOnline = false;
bool isSearchLoading = false;

Future<void> initSession(String token) async {
_ws.disconnect();

inbox.clear();
activeChat.clear();
contactSearchResults.clear();
currentChatUserId = null;
isPeerTyping = false;
isPeerOnline = false;
notifyListeners();

await _ws.connect(token);
_ws.stream?.listen(
(rawFrame) {
try {
final decoded = jsonDecode(rawFrame);
if (decoded is Map<String, dynamic>) {
_handleIncomingWebSocketEvent(decoded);
}
_handleIncomingWebSocketEvent(jsonDecode(rawFrame));
} catch (e) {
debugPrint("WebSocket payload error: $e");
debugPrint("WebSocket stream payload error: $e");
}
},
onError: (err) => debugPrint("WS Pipeline Error: $err"),
Expand All @@ -37,13 +43,32 @@ class ChatController extends ChangeNotifier {
await loadInbox();
}

void clearSessionData() {
_ws.disconnect();
inbox.clear();
activeChat.clear();
contactSearchResults.clear();
currentChatUserId = null;
isPeerTyping = false;
isPeerOnline = false;
notifyListeners();
}

Future<void> loadInbox() async {
try {
final res = await _api.getConversations();
final targetList = _extractDataList(res.data, ['conversations']);
dynamic targetData;

inbox = targetList.map((json) => Conversation.fromJson(json)).toList();
notifyListeners();
if (res.data is List) {
targetData = res.data;
} else if (res.data is Map) {
targetData = res.data['data'] ?? res.data['conversations'] ?? res.data;
}

if (targetData is List) {
inbox = targetData.map((json) => Conversation.fromJson(json)).toList();
notifyListeners();
}
} catch (e) {
debugPrint("Inbox read error: $e");
}
Expand All @@ -58,34 +83,33 @@ class ChatController extends ChangeNotifier {

try {
final res = await _api.getChatHistory(targetUid);
final targetList = _extractDataList(res.data, ['messages']);
dynamic targetData;

activeChat = targetList
.map((json) => Message.fromJson(json))
.toList()
.reversed
.toList();
if (res.data is List) {
targetData = res.data;
} else if (res.data is Map) {
targetData = res.data['data'] ?? res.data['messages'] ?? res.data;
}

if (targetData is List) {
activeChat = targetData
.map((json) => Message.fromJson(json))
.toList()
.reversed
.toList();
}

_ws.sendReadReceipt(targetId: targetUid);
_ws.sendRequestStatus(targetId: targetUid);

notifyListeners();
await loadInbox();
} catch (e) {
debugPrint("Timeline tracking fail: $e");
notifyListeners();
}
notifyListeners();
}

Future<void> queryUsers(String term) async {
final String cleanTerm = term.trim();

if (cleanTerm.isEmpty) {
contactSearchResults.clear();
notifyListeners();
return;
}

if (cleanTerm.length < 3) {
contactSearchResults.clear();
notifyListeners();
Expand All @@ -96,10 +120,22 @@ class ChatController extends ChangeNotifier {
notifyListeners();

try {
final res = await _api.searchUsers(term);
contactSearchResults = _extractDataList(res.data, ['users']);
final res = await _api.searchUsers(cleanTerm);
if (res.data == null) {
contactSearchResults = [];
} else if (res.data is List) {
contactSearchResults = res.data;
} else if (res.data is Map) {
if (res.data['data'] != null && res.data['data'] is List) {
contactSearchResults = res.data['data'];
} else if (res.data['users'] != null && res.data['users'] is List) {
contactSearchResults = res.data['users'];
} else {
contactSearchResults = [];
}
}
} catch (e) {
debugPrint("User query failure: $e");
debugPrint("User query pipeline failure: $e");
contactSearchResults.clear();
} finally {
isSearchLoading = false;
Expand All @@ -116,15 +152,16 @@ class ChatController extends ChangeNotifier {
}

void sendTextMessage(String text) {
final cleanContent = text.trim();
if (currentChatUserId == null || cleanContent.isEmpty) return;
if (currentChatUserId == null || text.trim().isEmpty) return;

final targetId = currentChatUserId!;
final clientMessageId = "cli_${DateTime.now().millisecondsSinceEpoch}";
final String clientMessageId =
"cli_${DateTime.now().millisecondsSinceEpoch}";
final String targetId = currentChatUserId!;
final String cleanContent = text.trim();

_ws.sendChat(messageId: "", targetId: targetId, content: cleanContent);

final optimisticMsg = Message(
final Message optimisticMsg = Message(
id: clientMessageId,
senderId: "me",
receiverId: targetId,
Expand All @@ -135,7 +172,6 @@ class ChatController extends ChangeNotifier {

activeChat.add(optimisticMsg);
notifyListeners();

loadInbox();
}

Expand All @@ -147,14 +183,13 @@ class ChatController extends ChangeNotifier {

void _handleIncomingWebSocketEvent(Map<String, dynamic> data) {
final String? type = data['type'];
if (type == null) return;

final String? senderId =
data['sender_id'] ?? data['sender'] ?? data['receiver_id'];

if (type == null) return;

final String? cleanSender = senderId?.trim().toLowerCase();
final String? cleanCurrentChat = currentChatUserId?.trim().toLowerCase();
final bool isCurrentChat =
cleanSender != null && cleanSender == cleanCurrentChat;

switch (type) {
case 'user_status':
Expand All @@ -168,54 +203,35 @@ class ChatController extends ChangeNotifier {
notifyListeners();
}
break;

case 'chat':
case 'message':
if (isCurrentChat) {
if (cleanSender != null && cleanSender == cleanCurrentChat) {
activeChat.add(Message.fromJson(data));
_ws.sendReadReceipt(targetId: currentChatUserId!);
notifyListeners();
} else {
loadInbox();
}
loadInbox();
break;

case 'typing':
if (isCurrentChat) {
if (cleanSender != null && cleanSender == cleanCurrentChat) {
isPeerTyping = data['content'] == 'true';
notifyListeners();
}
break;

case 'read_receipt':
if (isCurrentChat) {
bool updated = false;
if (cleanSender != null && cleanSender == cleanCurrentChat) {
activeChat = activeChat.map((msg) {
final String sId = msg.senderId.trim().toLowerCase();
if (!msg.isRead && (sId == 'me' || sId != cleanCurrentChat)) {
updated = true;
if (sId == 'me' || sId != cleanCurrentChat) {
return msg.copyWith(isRead: true);
}
return msg;
}).toList();

if (updated) {
notifyListeners();
loadInbox();
}
notifyListeners();
loadInbox();
}
break;
}
}

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 [];
}
}
50 changes: 35 additions & 15 deletions mobile/lib/pages/settings pages/accounts.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:mobile/controllers/auth.dart';
import 'package:mobile/controllers/chat.dart';
import 'package:mobile/providers/basic_providers.dart';
import 'package:mobile/main.dart';
import 'package:provider/provider.dart';

Expand All @@ -9,37 +11,55 @@ class AccountsSettings extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Accounts')),
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('Accounts'),
backgroundColor: Colors.white,
elevation: 0,
scrolledUnderElevation: 0,
),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
children: [
SizedBox(
const SizedBox(
width: double.infinity,
height: 150,
child: Hero(
tag: 'User Profile',
child: const Padding(
child: Padding(
padding: EdgeInsets.only(left: 16.0, top: 10.0, bottom: 10.0),
child: CircleAvatar(
backgroundColor: Color(0xFFD6E4FF),
child: Icon(Icons.person, color: Color(0xFF1890FF)),
child: Icon(Icons.person, color: Color(0xFF1890FF), size: 40),
),
),
),
),
SizedBox(height: 200),
const SizedBox(height: 100),
FilledButton(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(Colors.red),
style: const ButtonStyle(
backgroundColor: WidgetStatePropertyAll(Colors.redAccent),
),
onPressed: () {
context.read<AuthState>().logout();
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => SessionGateway()),
(route) => false,
);
onPressed: () async {
await context.read<AuthState>().logout();
await context.read<BasicProviders>().logOutSave();

if (context.mounted) {
context.read<ChatController>().clearSessionData();

Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) => const SessionGateway(),
),
(route) => false,
);
}
},
child: Text('Log out'),
child: const Text(
'Log out',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
Expand Down
Loading