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
10 changes: 9 additions & 1 deletion mobile/lib/controllers/chat.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ class ChatController extends ChangeNotifier {
bool isPeerTyping = false;
bool isPeerOnline = false;
bool isSearchLoading = false;
bool _isWsInitialized = false;

Future<void> initSession(String token) async {
if (_isWsInitialized) return;
_isWsInitialized = true;

await _ws.connect(token);
_ws.stream?.listen(
(rawFrame) {
Expand All @@ -34,8 +38,12 @@ class ChatController extends ChangeNotifier {
}
},
onError: (err) => debugPrint("WS Pipeline Error: $err"),
onDone: () => _ws.disconnect(),
onDone: () {
_ws.disconnect();
_isWsInitialized = false;
},
);

await loadInbox();
}

Expand Down
2 changes: 2 additions & 0 deletions mobile/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:mobile/providers/basic_providers.dart';
import 'package:mobile/providers/group_controller_provider.dart';
import 'package:provider/provider.dart';
import 'core/constants.dart';
import 'controllers/auth.dart';
Expand All @@ -16,6 +17,7 @@ void main() async {
ChangeNotifierProvider(create: (_) => BasicProviders()),
ChangeNotifierProvider(create: (_) => AuthState()),
ChangeNotifierProvider(create: (_) => ChatController()),
ChangeNotifierProvider(create: (_) => GroupController()),
],
child: const MyApp(),
),
Expand Down
135 changes: 132 additions & 3 deletions mobile/lib/pages/select_contact_page.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mobile/controllers/chat.dart';
import 'package:mobile/pages/chat_page.dart';
import 'package:mobile/providers/group_controller_provider.dart';
import 'package:provider/provider.dart';

class SelectContactPage extends StatefulWidget {
Expand All @@ -15,7 +17,9 @@ class _SelectContactPageState extends State<SelectContactPage> {

Timer? _debounceTimer;
bool _isSearchOpen = false;

final _searchController = TextEditingController();
final _groupNameController = TextEditingController();

void _onSearchChanged(String value, ChatController chatState) {
if (_debounceTimer?.isActive ?? false) _debounceTimer!.cancel();
Expand All @@ -42,6 +46,7 @@ class _SelectContactPageState extends State<SelectContactPage> {
void dispose() {
_debounceTimer?.cancel();
_searchController.dispose();
_groupNameController.dispose();
super.dispose();
}

Expand Down Expand Up @@ -270,7 +275,6 @@ class _SelectContactPageState extends State<SelectContactPage> {
),
),
],

const Padding(
padding: EdgeInsets.only(left: 16, top: 20, bottom: 8),
child: Text(
Expand All @@ -282,7 +286,6 @@ class _SelectContactPageState extends State<SelectContactPage> {
),
),
),

if (chatState.inbox.isNotEmpty)
...chatState.inbox
.where(
Expand All @@ -304,10 +307,136 @@ class _SelectContactPageState extends State<SelectContactPage> {
),
floatingActionButton: _selectedContacts.isNotEmpty
? FloatingActionButton(
onPressed: () {},
backgroundColor: Colors.blue,
elevation: 4,
child: const Icon(Icons.arrow_forward, color: Colors.white),
onPressed: () {
context.read<GroupController>().addContacts(_selectedContacts);

showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(16),
),
),
builder: (BuildContext bottomSheetContext) {
return Consumer<GroupController>(
builder: (context, groupState, child) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(
bottomSheetContext,
).viewInsets.bottom,
left: 16,
right: 16,
top: 24,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'New Group',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextField(
controller: _groupNameController,
autofocus: true,
decoration: const InputDecoration(
hintText: 'Enter group name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FilledButton(
onPressed: groupState.isLoading
? null
: () async {
final groupName =
_groupNameController.text
.trim();
if (groupName.isEmpty) return;

final memberIds = _selectedContacts
.keys
.toList();

final newGroup = await context
.read<GroupController>()
.createGroup(
groupName: groupName,
memberIds: memberIds,
);

if (newGroup != null) {
if (!context.mounted) return;

context
.read<ChatController>()
.loadInbox();

Navigator.of(
bottomSheetContext,
).pop();
Navigator.of(
context,
).pushReplacement(
MaterialPageRoute(
builder: (context) => ChatPage(
chatUserId: newGroup
.id,
displayName: newGroup
.name,
),
),
);

context
.read<ChatController>()
.openChat(newGroup.id);
} else {
if (!context.mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(
const SnackBar(
content: Text(
'Failed to create group',
),
),
);
}
},
child: groupState.isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Text('Create'),
),
],
),
const SizedBox(height: 16),
],
),
);
},
);
},
);
},
)
: null,
);
Expand Down
1 change: 0 additions & 1 deletion mobile/lib/pages/settings pages/accounts.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
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 Down
79 changes: 79 additions & 0 deletions mobile/lib/providers/group_controller_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import 'package:dio/dio.dart';
import 'package:flutter/widgets.dart';
import 'package:mobile/models/group.dart';
import 'package:mobile/services/api.dart';

class GroupController extends ChangeNotifier {
final Map<String, Map<String, String>> _selectedContacts = {};

Map<String, Map<String, String>> get selectedContacts => _selectedContacts;

void addContacts(Map<String, Map<String, String>> contacts) {
_selectedContacts.clear();
_selectedContacts.addAll(contacts);
notifyListeners();
}

// Group Creation

final ApiService _api = ApiService();

bool _isLoading = false;
bool get isLoading => _isLoading;

Future<Group?> createGroup({
required String groupName,
required List<String> memberIds,
}) async {
if (_isLoading) return null;

try {
_isLoading = true;
notifyListeners();

final createResponse = await _api.post(
'/groups',
data: {"name": groupName},
);

if (createResponse.statusCode == 200 ||
createResponse.statusCode == 201) {
final groupData =
createResponse.data['data'] ??
createResponse.data['group'] ??
createResponse.data;

final Group newGroup = Group.fromJson(groupData);

if (memberIds.isNotEmpty) {
final memberRequests = memberIds.map((userId) {
return _api.post(
'/groups/${newGroup.id}/members',
data: {"user_id": userId},
);
});

await Future.wait(memberRequests);
}

return newGroup;
}

return null;
} on DioException catch (e) {
debugPrint("Dio Error: ${e.message}");
debugPrint("URL: ${e.requestOptions.uri}");

if (e.response != null) {
debugPrint("Payload: ${e.response?.data}");
}
return null;
} catch (e) {
debugPrint("Error: $e");
return null;
} finally {
_isLoading = false;
notifyListeners();
}
}
}
8 changes: 8 additions & 0 deletions mobile/lib/services/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,12 @@ class ApiService {
},
);
}

Future<Response> post(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
}) async {
return await _dio.post(path, data: data, queryParameters: queryParameters);
}
}
Loading
Loading