diff --git a/lib/main.dart b/lib/main.dart index 9737c50..46f6f27 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart' hide TextDirection; @@ -13,6 +14,7 @@ import 'package:island/pods/network.dart'; import 'package:island/pods/theme.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:island/pods/userinfo.dart'; +import 'package:island/pods/websocket.dart'; import 'package:island/route.dart'; import 'package:island/services/notify.dart'; import 'package:island/widgets/app_scaffold.dart'; @@ -73,12 +75,17 @@ class IslandApp extends HookConsumerWidget { useEffect(() { // Load userinfo final userNotifier = ref.read(userInfoProvider.notifier); + ref.listen(websocketStateProvider, (_, state) { + log('[WebSocket] $state'); + }); Future(() { userNotifier.fetchUser().then((_) { final user = ref.watch(userInfoProvider); if (user.hasValue) { final apiClient = ref.read(apiClientProvider); subscribePushNotification(apiClient); + final wsNotifier = ref.read(websocketStateProvider.notifier); + wsNotifier.connect(); } }); }); diff --git a/lib/pods/websocket.dart b/lib/pods/websocket.dart new file mode 100644 index 0000000..d305ad5 --- /dev/null +++ b/lib/pods/websocket.dart @@ -0,0 +1,98 @@ +import 'dart:developer'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:island/pods/config.dart'; +import 'package:island/pods/network.dart'; +import 'package:web_socket_channel/io.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +part 'websocket.freezed.dart'; + +@freezed +class WebSocketState with _$WebSocketState { + const factory WebSocketState.connected() = _Connected; + const factory WebSocketState.connecting() = _Connecting; + const factory WebSocketState.disconnected() = _Disconnected; + const factory WebSocketState.error(String message) = _Error; +} + +final websocketProvider = Provider((ref) { + return WebSocketService(); +}); + +class WebSocketService { + WebSocketChannel? _channel; + Stream? _broadcastStream; + + Future connect(String url, String atk) async { + log('[WebSocket] Trying connecting to $url'); + try { + _channel = IOWebSocketChannel.connect( + Uri.parse(url), + headers: {'Authorization': 'Bearer $atk'}, + ); + await _channel!.ready; + _broadcastStream = _channel!.stream.asBroadcastStream(); + } catch (err) { + log('[WebSocket] Failed to connect: $err'); + } + } + + WebSocketChannel? get ws => _channel; + Stream get stream => _broadcastStream!; + + void sendMessage(String message) { + _channel!.sink.add(message); + } + + void close() { + _channel?.sink.close(); + } +} + +final websocketStateProvider = + StateNotifierProvider( + (ref) => WebSocketStateNotifier(ref), + ); + +class WebSocketStateNotifier extends StateNotifier { + final Ref ref; + + WebSocketStateNotifier(this.ref) : super(const WebSocketState.disconnected()); + + Future connect() async { + state = const WebSocketState.connecting(); + try { + final service = ref.read(websocketProvider); + final baseUrl = ref.watch(serverUrlProvider); + final atk = await getFreshAtk( + ref.watch(tokenPairProvider), + baseUrl, + onRefreshed: (atk, rtk) { + setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk); + ref.invalidate(tokenPairProvider); + }, + ); + if (atk == null) { + state = const WebSocketState.error('Unauthorized'); + return; + } + await service.connect('$baseUrl/ws'.replaceFirst('http', 'ws'), atk); + state = const WebSocketState.connected(); + } catch (err) { + state = WebSocketState.error('Failed to connect: $err'); + } + } + + void sendMessage(String message) { + final service = ref.read(websocketProvider); + service.sendMessage(message); + } + + void close() { + final service = ref.read(websocketProvider); + service.close(); + state = const WebSocketState.disconnected(); + } +} diff --git a/lib/pods/websocket.freezed.dart b/lib/pods/websocket.freezed.dart new file mode 100644 index 0000000..8837a1c --- /dev/null +++ b/lib/pods/websocket.freezed.dart @@ -0,0 +1,207 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'websocket.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$WebSocketState { + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is WebSocketState); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'WebSocketState()'; +} + + +} + +/// @nodoc +class $WebSocketStateCopyWith<$Res> { +$WebSocketStateCopyWith(WebSocketState _, $Res Function(WebSocketState) __); +} + + +/// @nodoc + + +class _Connected implements WebSocketState { + const _Connected(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Connected); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'WebSocketState.connected()'; +} + + +} + + + + +/// @nodoc + + +class _Connecting implements WebSocketState { + const _Connecting(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Connecting); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'WebSocketState.connecting()'; +} + + +} + + + + +/// @nodoc + + +class _Disconnected implements WebSocketState { + const _Disconnected(); + + + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Disconnected); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'WebSocketState.disconnected()'; +} + + +} + + + + +/// @nodoc + + +class _Error implements WebSocketState { + const _Error(this.message); + + + final String message; + +/// Create a copy of WebSocketState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ErrorCopyWith<_Error> get copyWith => __$ErrorCopyWithImpl<_Error>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Error&&(identical(other.message, message) || other.message == message)); +} + + +@override +int get hashCode => Object.hash(runtimeType,message); + +@override +String toString() { + return 'WebSocketState.error(message: $message)'; +} + + +} + +/// @nodoc +abstract mixin class _$ErrorCopyWith<$Res> implements $WebSocketStateCopyWith<$Res> { + factory _$ErrorCopyWith(_Error value, $Res Function(_Error) _then) = __$ErrorCopyWithImpl; +@useResult +$Res call({ + String message +}); + + + + +} +/// @nodoc +class __$ErrorCopyWithImpl<$Res> + implements _$ErrorCopyWith<$Res> { + __$ErrorCopyWithImpl(this._self, this._then); + + final _Error _self; + final $Res Function(_Error) _then; + +/// Create a copy of WebSocketState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? message = null,}) { + return _then(_Error( +null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/lib/screens/auth/login.dart b/lib/screens/auth/login.dart index 36d0fb1..fb6900c 100644 --- a/lib/screens/auth/login.dart +++ b/lib/screens/auth/login.dart @@ -11,6 +11,7 @@ import 'package:island/models/auth.dart'; import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/pods/userinfo.dart'; +import 'package:island/pods/websocket.dart'; import 'package:island/services/notify.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; @@ -136,6 +137,8 @@ class _LoginCheckScreen extends HookConsumerWidget { userNotifier.fetchUser().then((_) { final apiClient = ref.read(apiClientProvider); subscribePushNotification(apiClient); + final wsNotifier = ref.read(websocketStateProvider.notifier); + wsNotifier.connect(); }); Navigator.pop(context, true); } catch (err) { diff --git a/pubspec.lock b/pubspec.lock index f26bd8b..5369118 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1273,10 +1273,10 @@ packages: dependency: transitive description: name: provider - sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310" + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.1.5" pub_semver: dependency: transitive description: @@ -1844,7 +1844,7 @@ packages: source: hosted version: "1.0.0" web_socket_channel: - dependency: transitive + dependency: "direct main" description: name: web_socket_channel sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 diff --git a/pubspec.yaml b/pubspec.yaml index 9df4371..2f6926d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -83,6 +83,7 @@ dependencies: firebase_messaging: ^15.2.5 flutter_udid: ^4.0.0 firebase_core: ^3.13.0 + web_socket_channel: ^3.0.3 dev_dependencies: flutter_test: