Skip to content

Commit 18ba30c

Browse files
authored
feat(dms): Connect DMs to backend (#114)
1 parent 02aebd8 commit 18ba30c

34 files changed

Lines changed: 1563 additions & 564 deletions
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import 'dart:io';
2+
3+
import 'package:cookie_jar/cookie_jar.dart';
4+
import 'package:flutter_riverpod/flutter_riverpod.dart';
5+
import 'package:lam7a/core/constants/server_constant.dart';
6+
import 'package:lam7a/core/providers/authentication.dart';
7+
import 'package:lam7a/core/utils/logger.dart';
8+
import 'package:path_provider/path_provider.dart';
9+
import 'package:riverpod_annotation/riverpod_annotation.dart';
10+
import 'package:socket_io_client/socket_io_client.dart' as IO;
11+
import 'package:path/path.dart' as path;
12+
13+
part 'socket_service.g.dart';
14+
15+
16+
final socketServiceProvider = Provider<SocketService>((ref) => SocketService());
17+
18+
@Riverpod(keepAlive: true)
19+
void socketInitializer(Ref ref) {
20+
final auth = ref.watch(authenticationProvider);
21+
final socket = ref.read(socketServiceProvider);
22+
23+
if(auth.user != null){
24+
socket.connect();
25+
}else{
26+
socket.disconnect();
27+
}
28+
}
29+
30+
class SocketService {
31+
IO.Socket? _socket;
32+
33+
final Map<String, List<Function(dynamic)>> _listeners = {};
34+
35+
36+
final _logger = getLogger(SocketService);
37+
38+
Future<void> connect() async {
39+
if (_socket != null) {
40+
_logger.i('Disposing existing socket');
41+
try {
42+
_socket!.disconnect();
43+
} catch (_) {}
44+
try {
45+
_socket!.dispose();
46+
} catch (_) {}
47+
_socket = null;
48+
}
49+
50+
_logger.i("Connecting to socket service");
51+
52+
var cookieHeader = await getCookie();
53+
54+
_socket = IO.io(
55+
ServerConstant.serverURL,
56+
IO.OptionBuilder()
57+
.setTransports(['websocket'])
58+
.enableReconnection()
59+
.setExtraHeaders({'cookie': cookieHeader})
60+
.build(),
61+
);
62+
63+
_socket!.onError((err) => _logger.e(err.toString()));
64+
65+
_socket!.onConnect((_) => _logger.i('Connected to socket'));
66+
_socket!.onDisconnect((_) => _logger.i('Disconnected'));
67+
68+
_attachStoredListeners();
69+
}
70+
71+
Future<String> getCookie() async {
72+
final directory = await getApplicationDocumentsDirectory();
73+
final cookiePath = path.join(directory.path, '.cookies');
74+
await Directory(cookiePath).create(recursive: true);
75+
76+
final cookieJar = PersistCookieJar(storage: FileStorage(cookiePath));
77+
78+
final uri = Uri.parse(ServerConstant.serverURL); // ← your API / socket domain
79+
final cookies = await cookieJar.loadForRequest(uri);
80+
81+
if (cookies.isEmpty) {
82+
_logger.w('⚠️ No cookies found in jar for $uri');
83+
return "";
84+
}
85+
86+
final cookieHeader = cookies.map((c) => '${c.name}=${c.value}').join('; ');
87+
88+
return cookieHeader;
89+
}
90+
91+
void _attachStoredListeners() {
92+
if (_socket == null) return;
93+
_listeners.forEach((event, callbacks) {
94+
for (final cb in callbacks) {
95+
try {
96+
_socket!.on(event, cb);
97+
} catch (e, st) {
98+
_logger.w('Failed to attach listener for "$event": $e\n$st');
99+
}
100+
}
101+
});
102+
}
103+
104+
105+
void on(String event, Function(dynamic) callback) {
106+
// store callback
107+
_listeners.putIfAbsent(event, () => []).add(callback);
108+
109+
// attach to live socket if exists
110+
try {
111+
_socket?.on(event, callback);
112+
} catch (e) {
113+
_logger.w('Failed to attach on() for "$event": $e');
114+
}
115+
}
116+
117+
118+
void emit(String event, dynamic data) {
119+
if (_socket == null) {
120+
_logger.w('Attempted to emit while socket is null. Event: $event');
121+
return;
122+
}
123+
try {
124+
_socket!.emit(event, data);
125+
} catch (e) {
126+
_logger.e('Failed to emit "$event": $e');
127+
}
128+
}
129+
130+
131+
void off(String event, [Function(dynamic)? callback]) {
132+
if (callback == null) {
133+
_listeners.remove(event);
134+
try {
135+
_socket?.off(event);
136+
} catch (e) {
137+
_logger.w('Failed to off() for "$event": $e');
138+
}
139+
} else {
140+
_listeners[event]?.remove(callback);
141+
try {
142+
_socket?.off(event, callback);
143+
} catch (e) {
144+
_logger.w('Failed to off(event, callback) for "$event": $e');
145+
}
146+
}
147+
}
148+
149+
void disconnect() {
150+
_logger.i("Disconnecting from socket");
151+
_socket?.disconnect();
152+
}
153+
}

lam7a/lib/core/utils/logger.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import 'package:logger/logger.dart';
2+
3+
import 'package:logger/logger.dart';
4+
5+
class SimpleLogPrinter extends LogPrinter {
6+
final String className;
7+
8+
SimpleLogPrinter(this.className);
9+
10+
@override
11+
List<String> log(LogEvent event) {
12+
final color = PrettyPrinter.defaultLevelColors[event.level]!;
13+
final emoji = PrettyPrinter.defaultLevelEmojis[event.level];
14+
final message = event.message;
15+
final error = event.error;
16+
final stackTrace = event.stackTrace;
17+
18+
final buffer = StringBuffer();
19+
20+
buffer.write('${color('$emoji $className - $message')}');
21+
22+
if (error != null) {
23+
buffer.write('\n${color('Error: $error')}');
24+
}
25+
26+
if (stackTrace != null) {
27+
buffer.write('\n${color(stackTrace.toString())}');
28+
}
29+
30+
return [buffer.toString()];
31+
}
32+
}
33+
34+
Logger getLogger(Type type) {
35+
return Logger(printer: SimpleLogPrinter(type.toString()));
36+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import 'package:freezed_annotation/freezed_annotation.dart';
2+
import 'package:lam7a/features/messaging/dtos/conversation_dto.dart';
3+
4+
part 'api_response.freezed.dart';
5+
part 'api_response.g.dart';
6+
7+
@Freezed(genericArgumentFactories: true)
8+
abstract class ApiResponse<T> with _$ApiResponse<T> {
9+
const factory ApiResponse({
10+
required String status,
11+
required T data,
12+
required Metadata metadata,
13+
}) = _ApiResponse<T>;
14+
15+
factory ApiResponse.fromJson(
16+
Map<String, dynamic> json,
17+
T Function(Object?) fromJsonT,
18+
) => _$ApiResponseFromJson(json, fromJsonT);
19+
}
20+
21+
22+
class Metadata {
23+
final int totalItems;
24+
final int page;
25+
final int limit;
26+
final int totalPages;
27+
28+
Metadata({
29+
required this.totalItems,
30+
required this.page,
31+
required this.limit,
32+
required this.totalPages,
33+
});
34+
35+
factory Metadata.fromJson(Map<String, dynamic> json) => Metadata(
36+
totalItems: json['totalItems'],
37+
page: json['page'],
38+
limit: json['limit'],
39+
totalPages: json['totalPages'],
40+
);
41+
42+
Map<String, dynamic> toJson() => {
43+
'totalItems': totalItems,
44+
'page': page,
45+
'limit': limit,
46+
'totalPages': totalPages,
47+
};
48+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import 'package:freezed_annotation/freezed_annotation.dart';
2+
import 'package:lam7a/features/messaging/dtos/message_socket_dtos.dart';
3+
4+
part 'conversation_dto.freezed.dart';
5+
part 'conversation_dto.g.dart';
6+
7+
@freezed
8+
abstract class ConversationDto with _$ConversationDto {
9+
const factory ConversationDto({
10+
required int id,
11+
required String createdAt,
12+
required DateTime updatedAt,
13+
MessageDto? lastMessage,
14+
required UserDto user,
15+
}) = _ConversationDto;
16+
17+
factory ConversationDto.fromJson(Map<String, dynamic> json) =>
18+
_$ConversationDtoFromJson(json);
19+
}
20+
21+
22+
@freezed
23+
abstract class UserDto with _$UserDto {
24+
const factory UserDto({
25+
required int id,
26+
required String username,
27+
@JsonKey(name: 'profile_image_url') String? profileImageUrl,
28+
required String displayName,
29+
}) = _UserDto;
30+
31+
factory UserDto.fromJson(Map<String, dynamic> json) =>
32+
_$UserDtoFromJson(json);
33+
}

0 commit comments

Comments
 (0)