Skip to content

Commit f80c1c5

Browse files
Merge branch 'v0.2.0' into 37-group-chat-create-manage-send-messages-frontend
2 parents 54b7a17 + 9352e41 commit f80c1c5

6 files changed

Lines changed: 115 additions & 139 deletions

File tree

‎mobile/lib/controllers/auth.dart‎

Lines changed: 33 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -35,55 +35,33 @@ class AuthState extends ChangeNotifier {
3535

3636
try {
3737
final res = await _authService.login(username, password);
38-
3938
dynamic dataMap = res.data;
39+
4040
if (dataMap is String) {
4141
try {
4242
dataMap = jsonDecode(dataMap);
4343
} catch (_) {}
4444
}
4545

46-
if (dataMap is Map) {
47-
if (dataMap['success'] == true) {
48-
String? extractedToken;
49-
50-
if (dataMap['data'] != null && dataMap['data'] is Map) {
51-
final dataEnv = dataMap['data'];
52-
if (dataEnv['tokens'] != null && dataEnv['tokens'] is Map) {
53-
extractedToken = dataEnv['tokens']['access_token'];
54-
}
55-
}
56-
57-
if (extractedToken != null) {
58-
_token = extractedToken;
59-
await _storage.write(key: "access_token", value: _token);
60-
_isLoading = false;
61-
notifyListeners();
62-
return true;
63-
}
46+
if (dataMap is Map && dataMap['success'] == true) {
47+
Map<String, dynamic>? tokens = dataMap['data']?['tokens'];
48+
if (tokens != null &&
49+
tokens['access_token'] != null &&
50+
tokens['refresh_token'] != null) {
51+
_token = tokens['access_token'];
52+
await _authService.saveTokens(
53+
tokens['access_token'],
54+
tokens['refresh_token'],
55+
);
56+
_isLoading = false;
57+
notifyListeners();
58+
return true;
6459
}
65-
_errorMessage =
66-
dataMap['error'] ?? dataMap['message'] ?? "Authentication failed";
67-
} else {
68-
_errorMessage = "Unexpected server payload architecture structure.";
6960
}
61+
_errorMessage =
62+
dataMap['error'] ?? dataMap['message'] ?? "Authentication failed";
7063
} on DioException catch (e) {
71-
dynamic responseData = e.response?.data;
72-
if (responseData is String) {
73-
try {
74-
responseData = jsonDecode(responseData);
75-
} catch (_) {}
76-
}
77-
78-
if (responseData is Map) {
79-
_errorMessage =
80-
responseData['error'] ??
81-
responseData['message'] ??
82-
"Server validation failed.";
83-
} else {
84-
_errorMessage =
85-
"Server error code: ${e.response?.statusCode ?? 'unknown'}.";
86-
}
64+
_errorMessage = e.response?.data?['error'] ?? "Server validation failed.";
8765
} catch (e) {
8866
_errorMessage = "Connection parsing error occurred";
8967
}
@@ -102,110 +80,36 @@ class AuthState extends ChangeNotifier {
10280
_errorMessage = null;
10381
notifyListeners();
10482

105-
String? finalUsername;
106-
bool registerSuccess = false;
107-
10883
try {
10984
final res = await _authService.register(username, displayName, password);
110-
11185
dynamic dataMap = res.data;
86+
11287
if (dataMap is String) {
11388
try {
11489
dataMap = jsonDecode(dataMap);
11590
} catch (_) {}
11691
}
11792

118-
if (dataMap is Map) {
119-
if (dataMap['success'] == true) {
120-
String? extractedToken;
121-
122-
if (dataMap['data'] != null && dataMap['data'] is Map) {
123-
final dataEnv = dataMap['data'];
124-
if (dataEnv['tokens'] != null && dataEnv['tokens'] is Map) {
125-
extractedToken = dataEnv['tokens']['access_token'];
126-
}
127-
if (dataEnv['user'] != null && dataEnv['user'] is Map) {
128-
finalUsername = dataEnv['user']['username'];
129-
}
130-
}
131-
132-
if (extractedToken != null) {
133-
_token = extractedToken;
134-
await _storage.write(key: "access_token", value: _token);
135-
_isLoading = false;
136-
notifyListeners();
137-
return true;
138-
}
139-
140-
registerSuccess = true;
141-
} else {
142-
_errorMessage =
143-
dataMap['error'] ?? dataMap['message'] ?? "Registration failed";
93+
if (dataMap is Map && dataMap['success'] == true) {
94+
Map<String, dynamic>? tokens = dataMap['data']?['tokens'];
95+
if (tokens != null &&
96+
tokens['access_token'] != null &&
97+
tokens['refresh_token'] != null) {
98+
_token = tokens['access_token'];
99+
await _authService.saveTokens(
100+
tokens['access_token'],
101+
tokens['refresh_token'],
102+
);
144103
_isLoading = false;
145104
notifyListeners();
146-
return false;
105+
return true;
147106
}
148107
}
108+
_errorMessage = dataMap['error'] ?? "Registration failed";
149109
} on DioException catch (e) {
150-
dynamic responseData = e.response?.data;
151-
if (responseData is String) {
152-
try {
153-
responseData = jsonDecode(responseData);
154-
} catch (_) {}
155-
}
156-
if (responseData is Map) {
157-
_errorMessage =
158-
responseData['error'] ??
159-
responseData['message'] ??
160-
"Registration rejected.";
161-
} else {
162-
_errorMessage =
163-
"Registration server error: ${e.response?.statusCode ?? 'unknown'}.";
164-
}
165-
_isLoading = false;
166-
notifyListeners();
167-
return false;
110+
_errorMessage = e.response?.data?['error'] ?? "Registration rejected.";
168111
} catch (e) {
169112
_errorMessage = "Server error occurred during sign up";
170-
_isLoading = false;
171-
notifyListeners();
172-
return false;
173-
}
174-
175-
if (registerSuccess) {
176-
try {
177-
final String targetUsername = finalUsername ?? username;
178-
final loginRes = await _authService.login(targetUsername, password);
179-
180-
dynamic loginData = loginRes.data;
181-
if (loginData is String) {
182-
try {
183-
loginData = jsonDecode(loginData);
184-
} catch (_) {}
185-
}
186-
187-
if (loginData is Map && loginData['success'] == true) {
188-
String? loginToken;
189-
if (loginData['data'] != null && loginData['data'] is Map) {
190-
final dataEnv = loginData['data'];
191-
if (dataEnv['tokens'] != null && dataEnv['tokens'] is Map) {
192-
loginToken = dataEnv['tokens']['access_token'];
193-
}
194-
}
195-
196-
if (loginToken != null) {
197-
_token = loginToken;
198-
await _storage.write(key: "access_token", value: _token);
199-
_isLoading = false;
200-
notifyListeners();
201-
return true;
202-
}
203-
}
204-
_errorMessage =
205-
"Account created successfully as $targetUsername! Please log in manually.";
206-
} catch (e) {
207-
_errorMessage = "Profile configured! Please switch tabs to log in.";
208-
}
209113
}
210114

211115
_isLoading = false;
@@ -215,14 +119,14 @@ class AuthState extends ChangeNotifier {
215119

216120
Future<void> logout() async {
217121
_token = null;
218-
await _storage.delete(key: "access_token");
122+
await _authService.logout();
219123
notifyListeners();
220124
}
221125

222126
void logoutSilently() {
223127
if (_token != null) {
224128
_token = null;
225-
_storage.delete(key: "access_token");
129+
_authService.logout();
226130
notifyListeners();
227131
}
228132
}

‎mobile/lib/controllers/chat.dart‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,15 @@ class ChatController extends ChangeNotifier {
7878
}
7979

8080
Future<void> queryUsers(String term) async {
81-
if (term.trim().isEmpty) {
81+
final String cleanTerm = term.trim();
82+
83+
if (cleanTerm.isEmpty) {
84+
contactSearchResults.clear();
85+
notifyListeners();
86+
return;
87+
}
88+
89+
if (cleanTerm.length < 3) {
8290
contactSearchResults.clear();
8391
notifyListeners();
8492
return;

‎mobile/lib/pages/new_chat_page.dart‎

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class _NewChatPageState extends State<NewChatPage> {
5050
autofocus: true,
5151
style: const TextStyle(color: Colors.black87),
5252
decoration: const InputDecoration(
53-
hintText: "Enter exact username...",
53+
hintText: "Enter minimum 3 characters...",
5454
hintStyle: TextStyle(color: Colors.black38),
5555
enabledBorder: UnderlineInputBorder(
5656
borderSide: BorderSide(color: Colors.black26),
@@ -74,13 +74,22 @@ class _NewChatPageState extends State<NewChatPage> {
7474
),
7575
onPressed: () {
7676
final text = inputController.text.trim();
77-
if (text.isNotEmpty) {
77+
if (text.length >= 3) {
7878
context.read<ChatController>().queryUsers(text);
7979
setState(() {
8080
_searchController.text = text;
8181
});
82+
Navigator.pop(context);
83+
} else {
84+
ScaffoldMessenger.of(context).showSnackBar(
85+
const SnackBar(
86+
content: Text(
87+
"Search query must be at least 3 characters.",
88+
),
89+
backgroundColor: Colors.redAccent,
90+
),
91+
);
8292
}
83-
Navigator.pop(context);
8493
},
8594
child: const Text("Search", style: TextStyle(color: Colors.white)),
8695
),
@@ -116,7 +125,9 @@ class _NewChatPageState extends State<NewChatPage> {
116125
@override
117126
Widget build(BuildContext context) {
118127
final chatState = context.watch<ChatController>();
119-
final bool isSearching = _searchController.text.trim().isNotEmpty;
128+
final String searchInput = _searchController.text.trim();
129+
final bool isSearching = searchInput.isNotEmpty;
130+
final bool hasValidQueryLength = searchInput.length >= 3;
120131

121132
return Scaffold(
122133
backgroundColor: Colors.white,
@@ -174,7 +185,17 @@ class _NewChatPageState extends State<NewChatPage> {
174185
),
175186
Expanded(
176187
child: isSearching
177-
? (chatState.isSearchLoading
188+
? (!hasValidQueryLength
189+
? const Center(
190+
child: Text(
191+
"Type at least 3 characters to search...",
192+
style: TextStyle(
193+
color: Colors.black45,
194+
fontSize: 14,
195+
),
196+
),
197+
)
198+
: chatState.isSearchLoading
178199
? const Center(
179200
child: CircularProgressIndicator(
180201
color: Color(0xFF1890FF),

‎mobile/lib/services/api.dart‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,38 @@ class ApiService {
2121
onResponse: (response, handler) {
2222
return handler.next(response);
2323
},
24-
onError: (DioException e, handler) {
24+
onError: (DioException e, handler) async {
2525
if (e.response?.statusCode == 401) {
26+
final refreshToken = await _auth.getRefreshToken();
27+
28+
if (refreshToken != null) {
29+
try {
30+
final refreshResponse = await _auth.refreshAccessToken(
31+
refreshToken,
32+
);
33+
final resData = refreshResponse.data;
34+
35+
if (resData['success'] == true) {
36+
final newAccessToken = resData['data']['access_token'];
37+
final newRefreshToken = resData['data']['refresh_token'];
38+
39+
await _auth.saveTokens(newAccessToken, newRefreshToken);
40+
41+
final cloneOptions = e.requestOptions;
42+
cloneOptions.headers["Authorization"] =
43+
"Bearer $newAccessToken";
44+
45+
final retryResponse = await _dio.fetch(cloneOptions);
46+
return handler.resolve(retryResponse);
47+
}
48+
} catch (refreshError) {
49+
if (AuthState.onGlobalUnauthorized != null) {
50+
AuthState.onGlobalUnauthorized!();
51+
}
52+
return handler.next(e);
53+
}
54+
}
55+
2656
if (AuthState.onGlobalUnauthorized != null) {
2757
AuthState.onGlobalUnauthorized!();
2858
}

‎mobile/lib/services/auth.dart‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,18 @@ class AuthService {
3333
return await _storage.read(key: "access_token");
3434
}
3535

36-
Future<void> saveToken(String token) async {
37-
await _storage.write(key: "access_token", value: token);
36+
Future<String?> getRefreshToken() async {
37+
return await _storage.read(key: "refresh_token");
38+
}
39+
40+
Future<void> saveTokens(String accessToken, String refreshToken) async {
41+
await _storage.write(key: "access_token", value: accessToken);
42+
await _storage.write(key: "refresh_token", value: refreshToken);
3843
}
3944

4045
Future<void> logout() async {
4146
await _storage.delete(key: "access_token");
47+
await _storage.delete(key: "refresh_token");
4248
}
4349

4450
Future<Response> login(String username, String password) async {
@@ -62,4 +68,11 @@ class AuthService {
6268
},
6369
);
6470
}
71+
72+
Future<Response> refreshAccessToken(String refreshToken) async {
73+
return await _dio.post(
74+
"/auth/refresh",
75+
data: {"refresh_token": refreshToken},
76+
);
77+
}
6578
}

server/migrations/007_add_message_reply_to_up.sql renamed to server/migrations/008_add_message_reply_to_up.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ ALTER TABLE messages ADD COLUMN reply_to_message_id UUID REFERENCES messages(id)
22

33
CREATE INDEX IF NOT EXISTS idx_messages_reply_to
44
ON messages(reply_to_message_id)
5-
WHERE reply_to_message_id IS NOT NULL;
5+
WHERE reply_to_message_id IS NOT NULL;

0 commit comments

Comments
 (0)