Chat room details, invitions and members management

This commit is contained in:
2025-05-03 20:42:37 +08:00
parent e2e6de965b
commit efdddf72e4
26 changed files with 1915 additions and 201 deletions

View File

@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart';
@ -21,6 +23,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
final user = SnAccount.fromJson(response.data);
state = AsyncValue.data(user);
} catch (error, stackTrace) {
log("[UserInfo] Failed to fetch user info: $error");
state = AsyncValue.error(error, stackTrace);
}
}
@ -29,6 +32,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
state = const AsyncValue.data(null);
final prefs = _ref.read(sharedPreferencesProvider);
await prefs.remove(kTokenPairStoreKey);
_ref.refresh(userInfoProvider.notifier);
}
}

View File

@ -1,4 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:typed_data';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@ -8,6 +11,7 @@ import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
part 'websocket.freezed.dart';
part 'websocket.g.dart';
@freezed
class WebSocketState with _$WebSocketState {
@ -17,15 +21,35 @@ class WebSocketState with _$WebSocketState {
const factory WebSocketState.error(String message) = _Error;
}
@freezed
abstract class WebSocketPacket with _$WebSocketPacket {
const factory WebSocketPacket({
required String type,
required Map<String, dynamic>? data,
required String? errorMessage,
}) = _WebSocketPacket;
factory WebSocketPacket.fromJson(Map<String, dynamic> json) =>
_$WebSocketPacketFromJson(json);
}
final websocketProvider = Provider<WebSocketService>((ref) {
return WebSocketService();
});
class WebSocketService {
WebSocketChannel? _channel;
Stream<dynamic>? _broadcastStream;
final StreamController<WebSocketPacket> _streamController =
StreamController<WebSocketPacket>.broadcast();
String? _lastUrl;
String? _lastAtk;
Timer? _reconnectTimer;
Stream<WebSocketPacket> get dataStream => _streamController.stream;
Future<void> connect(String url, String atk) async {
_lastUrl = url;
_lastAtk = atk;
log('[WebSocket] Trying connecting to $url');
try {
_channel = IOWebSocketChannel.connect(
@ -33,20 +57,48 @@ class WebSocketService {
headers: {'Authorization': 'Bearer $atk'},
);
await _channel!.ready;
_broadcastStream = _channel!.stream.asBroadcastStream();
_channel!.stream.listen(
(data) {
final dataStr =
data is Uint8List ? utf8.decode(data) : data.toString();
final packet = WebSocketPacket.fromJson(jsonDecode(dataStr));
_streamController.sink.add(packet);
log("[WebSocket] Received packet: ${packet.type}");
},
onDone: () {
log('[WebSocket] Connection closed, attempting to reconnect...');
_scheduleReconnect();
},
onError: (error) {
log('[WebSocket] Error occurred: $error, attempting to reconnect...');
_scheduleReconnect();
},
);
} catch (err) {
log('[WebSocket] Failed to connect: $err');
_scheduleReconnect();
}
}
void _scheduleReconnect() {
_reconnectTimer?.cancel();
_reconnectTimer = Timer(const Duration(milliseconds: 500), () {
if (_lastUrl != null && _lastAtk != null) {
connect(_lastUrl!, _lastAtk!);
}
});
}
WebSocketChannel? get ws => _channel;
Stream<dynamic> get stream => _broadcastStream!;
void sendMessage(String message) {
_channel!.sink.add(message);
}
void close() {
_reconnectTimer?.cancel();
_lastUrl = null;
_lastAtk = null;
_channel?.sink.close();
}
}

View File

@ -202,6 +202,153 @@ as String,
}
}
/// @nodoc
mixin _$WebSocketPacket {
String get type; Map<String, dynamic>? get data; String? get errorMessage;
/// Create a copy of WebSocketPacket
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$WebSocketPacketCopyWith<WebSocketPacket> get copyWith => _$WebSocketPacketCopyWithImpl<WebSocketPacket>(this as WebSocketPacket, _$identity);
/// Serializes this WebSocketPacket to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is WebSocketPacket&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(data),errorMessage);
@override
String toString() {
return 'WebSocketPacket(type: $type, data: $data, errorMessage: $errorMessage)';
}
}
/// @nodoc
abstract mixin class $WebSocketPacketCopyWith<$Res> {
factory $WebSocketPacketCopyWith(WebSocketPacket value, $Res Function(WebSocketPacket) _then) = _$WebSocketPacketCopyWithImpl;
@useResult
$Res call({
String type, Map<String, dynamic>? data, String? errorMessage
});
}
/// @nodoc
class _$WebSocketPacketCopyWithImpl<$Res>
implements $WebSocketPacketCopyWith<$Res> {
_$WebSocketPacketCopyWithImpl(this._self, this._then);
final WebSocketPacket _self;
final $Res Function(WebSocketPacket) _then;
/// Create a copy of WebSocketPacket
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? data = freezed,Object? errorMessage = freezed,}) {
return _then(_self.copyWith(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
@JsonSerializable()
class _WebSocketPacket implements WebSocketPacket {
const _WebSocketPacket({required this.type, required final Map<String, dynamic>? data, required this.errorMessage}): _data = data;
factory _WebSocketPacket.fromJson(Map<String, dynamic> json) => _$WebSocketPacketFromJson(json);
@override final String type;
final Map<String, dynamic>? _data;
@override Map<String, dynamic>? get data {
final value = _data;
if (value == null) return null;
if (_data is EqualUnmodifiableMapView) return _data;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
@override final String? errorMessage;
/// Create a copy of WebSocketPacket
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$WebSocketPacketCopyWith<_WebSocketPacket> get copyWith => __$WebSocketPacketCopyWithImpl<_WebSocketPacket>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$WebSocketPacketToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _WebSocketPacket&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._data, _data)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(_data),errorMessage);
@override
String toString() {
return 'WebSocketPacket(type: $type, data: $data, errorMessage: $errorMessage)';
}
}
/// @nodoc
abstract mixin class _$WebSocketPacketCopyWith<$Res> implements $WebSocketPacketCopyWith<$Res> {
factory _$WebSocketPacketCopyWith(_WebSocketPacket value, $Res Function(_WebSocketPacket) _then) = __$WebSocketPacketCopyWithImpl;
@override @useResult
$Res call({
String type, Map<String, dynamic>? data, String? errorMessage
});
}
/// @nodoc
class __$WebSocketPacketCopyWithImpl<$Res>
implements _$WebSocketPacketCopyWith<$Res> {
__$WebSocketPacketCopyWithImpl(this._self, this._then);
final _WebSocketPacket _self;
final $Res Function(_WebSocketPacket) _then;
/// Create a copy of WebSocketPacket
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? data = freezed,Object? errorMessage = freezed,}) {
return _then(_WebSocketPacket(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,data: freezed == data ? _self._data : data // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
// dart format on

21
lib/pods/websocket.g.dart Normal file
View File

@ -0,0 +1,21 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'websocket.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_WebSocketPacket _$WebSocketPacketFromJson(Map<String, dynamic> json) =>
_WebSocketPacket(
type: json['type'] as String,
data: json['data'] as Map<String, dynamic>?,
errorMessage: json['error_message'] as String?,
);
Map<String, dynamic> _$WebSocketPacketToJson(_WebSocketPacket instance) =>
<String, dynamic>{
'type': instance.type,
'data': instance.data,
'error_message': instance.errorMessage,
};