mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 08:22:16 +03:00
Convert StateNotifiers to AsyncNotifiers for App states.
This commit is contained in:
parent
fbe3cab253
commit
16f6732f09
@ -23,22 +23,19 @@ import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../management/state.dart';
|
||||
|
||||
final androidManagementState = StateNotifierProvider.autoDispose
|
||||
.family<ManagementStateNotifier, AsyncValue<DeviceInfo>, DevicePath>(
|
||||
(ref, devicePath) {
|
||||
// Make sure to rebuild if currentDevice changes (as on reboot)
|
||||
ref.watch(currentDeviceProvider);
|
||||
final notifier = _AndroidManagementStateNotifier(ref);
|
||||
return notifier..refresh();
|
||||
},
|
||||
final androidManagementState = AsyncNotifierProvider.autoDispose
|
||||
.family<ManagementStateNotifier, DeviceInfo, DevicePath>(
|
||||
_AndroidManagementStateNotifier.new,
|
||||
);
|
||||
|
||||
class _AndroidManagementStateNotifier extends ManagementStateNotifier {
|
||||
final Ref _ref;
|
||||
@override
|
||||
FutureOr<DeviceInfo> build(DevicePath devicePath) {
|
||||
// Make sure to rebuild if currentDevice changes (as on reboot)
|
||||
ref.watch(currentDeviceProvider);
|
||||
|
||||
_AndroidManagementStateNotifier(this._ref) : super();
|
||||
|
||||
void refresh() async {}
|
||||
return Completer<DeviceInfo>().future;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setMode(
|
||||
@ -55,6 +52,6 @@ class _AndroidManagementStateNotifier extends ManagementStateNotifier {
|
||||
state = const AsyncValue.loading();
|
||||
}
|
||||
|
||||
_ref.read(attachedDevicesProvider.notifier).refresh();
|
||||
ref.read(attachedDevicesProvider.notifier).refresh();
|
||||
}
|
||||
}
|
||||
|
@ -36,33 +36,31 @@ final _log = Logger('android.oath.state');
|
||||
|
||||
const _methods = MethodChannel('android.oath.methods');
|
||||
|
||||
final androidOathStateProvider = StateNotifierProvider.autoDispose
|
||||
.family<OathStateNotifier, AsyncValue<OathState>, DevicePath>(
|
||||
(ref, devicePath) => _AndroidOathStateNotifier());
|
||||
final androidOathStateProvider = AsyncNotifierProvider.autoDispose
|
||||
.family<OathStateNotifier, OathState, DevicePath>(
|
||||
_AndroidOathStateNotifier.new);
|
||||
|
||||
class _AndroidOathStateNotifier extends OathStateNotifier {
|
||||
final _events = const EventChannel('android.oath.sessionState');
|
||||
late StreamSubscription _sub;
|
||||
_AndroidOathStateNotifier() : super() {
|
||||
|
||||
@override
|
||||
FutureOr<OathState> build(DevicePath arg) {
|
||||
_sub = _events.receiveBroadcastStream().listen((event) {
|
||||
final json = jsonDecode(event);
|
||||
if (mounted) {
|
||||
if (json == null) {
|
||||
state = const AsyncValue.loading();
|
||||
} else {
|
||||
final oathState = OathState.fromJson(json);
|
||||
state = AsyncValue.data(oathState);
|
||||
}
|
||||
if (json == null) {
|
||||
state = const AsyncValue.loading();
|
||||
} else {
|
||||
final oathState = OathState.fromJson(json);
|
||||
state = AsyncValue.data(oathState);
|
||||
}
|
||||
}, onError: (err, stackTrace) {
|
||||
state = AsyncValue.error(err, stackTrace);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sub.cancel();
|
||||
super.dispose();
|
||||
ref.onDispose(_sub.cancel);
|
||||
|
||||
return Completer<OathState>().future;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -18,6 +18,8 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../app/models.dart';
|
||||
|
||||
bool get isDesktop {
|
||||
return const [
|
||||
TargetPlatform.windows,
|
||||
@ -36,21 +38,16 @@ final prefProvider = Provider<SharedPreferences>((ref) {
|
||||
});
|
||||
|
||||
abstract class ApplicationStateNotifier<T>
|
||||
extends StateNotifier<AsyncValue<T>> {
|
||||
ApplicationStateNotifier() : super(const AsyncValue.loading());
|
||||
extends AutoDisposeFamilyAsyncNotifier<T, DevicePath> {
|
||||
ApplicationStateNotifier() : super();
|
||||
|
||||
@protected
|
||||
Future<void> updateState(Future<T> Function() guarded) async {
|
||||
final result = await AsyncValue.guard(guarded);
|
||||
if (mounted) {
|
||||
state = result;
|
||||
}
|
||||
state = await AsyncValue.guard(guarded);
|
||||
}
|
||||
|
||||
@protected
|
||||
void setData(T value) {
|
||||
if (mounted) {
|
||||
state = AsyncValue.data(value);
|
||||
}
|
||||
state = AsyncValue.data(value);
|
||||
}
|
||||
}
|
||||
|
@ -45,47 +45,42 @@ final _sessionProvider =
|
||||
},
|
||||
);
|
||||
|
||||
final desktopFidoState = StateNotifierProvider.autoDispose
|
||||
.family<FidoStateNotifier, AsyncValue<FidoState>, DevicePath>(
|
||||
(ref, devicePath) {
|
||||
final session = ref.watch(_sessionProvider(devicePath));
|
||||
final desktopFidoState = AsyncNotifierProvider.autoDispose
|
||||
.family<FidoStateNotifier, FidoState, DevicePath>(
|
||||
_DesktopFidoStateNotifier.new);
|
||||
|
||||
class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
||||
late RpcNodeSession _session;
|
||||
late StateController<String?> _pinController;
|
||||
|
||||
@override
|
||||
FutureOr<FidoState> build(DevicePath devicePath) async {
|
||||
_session = ref.watch(_sessionProvider(devicePath));
|
||||
if (Platform.isWindows) {
|
||||
// Make sure to rebuild if isAdmin changes
|
||||
ref.watch(rpcStateProvider.select((state) => state.isAdmin));
|
||||
}
|
||||
final notifier = _DesktopFidoStateNotifier(
|
||||
session,
|
||||
ref.watch(_pinProvider(devicePath).notifier),
|
||||
);
|
||||
session.setErrorHandler('state-reset', (_) async {
|
||||
_pinController = ref.watch(_pinProvider(devicePath).notifier);
|
||||
_session.setErrorHandler('state-reset', (_) async {
|
||||
ref.invalidate(_sessionProvider(devicePath));
|
||||
});
|
||||
session.setErrorHandler('auth-required', (_) async {
|
||||
_session.setErrorHandler('auth-required', (_) async {
|
||||
final pin = ref.read(_pinProvider(devicePath));
|
||||
if (pin != null) {
|
||||
await notifier.unlock(pin);
|
||||
await unlock(pin);
|
||||
}
|
||||
});
|
||||
ref.onDispose(() {
|
||||
session.unsetErrorHandler('auth-required');
|
||||
_session.unsetErrorHandler('auth-required');
|
||||
});
|
||||
ref.onDispose(() {
|
||||
session.unsetErrorHandler('state-reset');
|
||||
_session.unsetErrorHandler('state-reset');
|
||||
});
|
||||
return notifier..refresh();
|
||||
},
|
||||
);
|
||||
|
||||
class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
||||
final RpcNodeSession _session;
|
||||
final StateController<String?> _pinController;
|
||||
_DesktopFidoStateNotifier(this._session, this._pinController) : super();
|
||||
|
||||
Future<void> refresh() => updateState(() async {
|
||||
final result = await _session.command('get');
|
||||
_log.debug('application status', jsonEncode(result));
|
||||
return FidoState.fromJson(result['data']);
|
||||
});
|
||||
final result = await _session.command('get');
|
||||
_log.debug('application status', jsonEncode(result));
|
||||
return FidoState.fromJson(result['data']);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<InteractionEvent> reset() {
|
||||
@ -105,8 +100,8 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
||||
controller.onListen = () async {
|
||||
try {
|
||||
await _session.command('reset', signal: signaler);
|
||||
await refresh();
|
||||
await controller.sink.close();
|
||||
ref.invalidateSelf();
|
||||
} catch (e) {
|
||||
controller.sink.addError(e);
|
||||
}
|
||||
@ -155,16 +150,19 @@ final desktopFingerprintProvider = StateNotifierProvider.autoDispose.family<
|
||||
FidoFingerprintsNotifier, AsyncValue<List<Fingerprint>>, DevicePath>(
|
||||
(ref, devicePath) => _DesktopFidoFingerprintsNotifier(
|
||||
ref.watch(_sessionProvider(devicePath)),
|
||||
ref,
|
||||
));
|
||||
|
||||
class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier {
|
||||
final RpcNodeSession _session;
|
||||
final Ref _ref;
|
||||
|
||||
_DesktopFidoFingerprintsNotifier(this._session) {
|
||||
_DesktopFidoFingerprintsNotifier(this._session, this._ref) {
|
||||
_refresh();
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
_ref.invalidate(fidoStateProvider(_session.devicePath));
|
||||
final result = await _session.command('fingerprints');
|
||||
setItems((result['children'] as Map<String, dynamic>)
|
||||
.entries
|
||||
@ -236,12 +234,14 @@ final desktopCredentialProvider = StateNotifierProvider.autoDispose.family<
|
||||
FidoCredentialsNotifier, AsyncValue<List<FidoCredential>>, DevicePath>(
|
||||
(ref, devicePath) => _DesktopFidoCredentialsNotifier(
|
||||
ref.watch(_sessionProvider(devicePath)),
|
||||
ref,
|
||||
));
|
||||
|
||||
class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier {
|
||||
final RpcNodeSession _session;
|
||||
final Ref _ref;
|
||||
|
||||
_DesktopFidoCredentialsNotifier(this._session) {
|
||||
_DesktopFidoCredentialsNotifier(this._session, this._ref) {
|
||||
_refresh();
|
||||
}
|
||||
|
||||
@ -259,6 +259,7 @@ class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier {
|
||||
}
|
||||
}
|
||||
setItems(creds);
|
||||
_ref.invalidate(fidoStateProvider(_session.devicePath));
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -36,53 +36,51 @@ final _sessionProvider =
|
||||
RpcNodeSession(ref.watch(rpcProvider).requireValue, devicePath, []),
|
||||
);
|
||||
|
||||
final desktopManagementState = StateNotifierProvider.autoDispose
|
||||
.family<ManagementStateNotifier, AsyncValue<DeviceInfo>, DevicePath>(
|
||||
(ref, devicePath) {
|
||||
final desktopManagementState = AsyncNotifierProvider.autoDispose
|
||||
.family<ManagementStateNotifier, DeviceInfo, DevicePath>(
|
||||
_DesktopManagementStateNotifier.new);
|
||||
|
||||
class _DesktopManagementStateNotifier extends ManagementStateNotifier {
|
||||
late RpcNodeSession _session;
|
||||
List<String> _subpath = [];
|
||||
_DesktopManagementStateNotifier() : super();
|
||||
|
||||
@override
|
||||
FutureOr<DeviceInfo> build(DevicePath devicePath) async {
|
||||
// Make sure to rebuild if currentDevice changes (as on reboot)
|
||||
ref.watch(currentDeviceProvider);
|
||||
final session = ref.watch(_sessionProvider(devicePath));
|
||||
final notifier = _DesktopManagementStateNotifier(ref, session);
|
||||
session.setErrorHandler('state-reset', (_) async {
|
||||
|
||||
_session = ref.watch(_sessionProvider(devicePath));
|
||||
_session.setErrorHandler('state-reset', (_) async {
|
||||
ref.invalidate(_sessionProvider(devicePath));
|
||||
});
|
||||
ref.onDispose(() {
|
||||
session.unsetErrorHandler('state-reset');
|
||||
_session.unsetErrorHandler('state-reset');
|
||||
});
|
||||
return notifier..refresh();
|
||||
},
|
||||
);
|
||||
|
||||
class _DesktopManagementStateNotifier extends ManagementStateNotifier {
|
||||
final Ref _ref;
|
||||
final RpcNodeSession _session;
|
||||
List<String> _subpath = [];
|
||||
_DesktopManagementStateNotifier(this._ref, this._session) : super();
|
||||
|
||||
Future<void> refresh() => updateState(() async {
|
||||
final result = await _session.command('get');
|
||||
final info = DeviceInfo.fromJson(result['data']['info']);
|
||||
final interfaces = (result['children'] as Map).keys.toSet();
|
||||
for (final iface in [
|
||||
// This is the preferred order
|
||||
UsbInterface.ccid,
|
||||
UsbInterface.otp,
|
||||
UsbInterface.fido,
|
||||
]) {
|
||||
if (interfaces.contains(iface.name)) {
|
||||
final path = [iface.name, 'management'];
|
||||
try {
|
||||
await _session.command('get', target: path);
|
||||
_subpath = path;
|
||||
_log.debug('Using transport $iface for management');
|
||||
return info;
|
||||
} catch (e) {
|
||||
_log.warning('Failed connecting to management via $iface');
|
||||
}
|
||||
}
|
||||
final result = await _session.command('get');
|
||||
final info = DeviceInfo.fromJson(result['data']['info']);
|
||||
final interfaces = (result['children'] as Map).keys.toSet();
|
||||
for (final iface in [
|
||||
// This is the preferred order
|
||||
UsbInterface.ccid,
|
||||
UsbInterface.otp,
|
||||
UsbInterface.fido,
|
||||
]) {
|
||||
if (interfaces.contains(iface.name)) {
|
||||
final path = [iface.name, 'management'];
|
||||
try {
|
||||
await _session.command('get', target: path);
|
||||
_subpath = path;
|
||||
_log.debug('Using transport $iface for management');
|
||||
return info;
|
||||
} catch (e) {
|
||||
_log.warning('Failed connecting to management via $iface');
|
||||
}
|
||||
throw 'Failed connection over all interfaces';
|
||||
});
|
||||
}
|
||||
}
|
||||
throw 'Failed connection over all interfaces';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setMode(
|
||||
@ -94,7 +92,7 @@ class _DesktopManagementStateNotifier extends ManagementStateNotifier {
|
||||
'challenge_response_timeout': challengeResponseTimeout,
|
||||
'auto_eject_timeout': autoEjectTimeout,
|
||||
});
|
||||
_ref.read(attachedDevicesProvider.notifier).refresh();
|
||||
ref.read(attachedDevicesProvider.notifier).refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -111,6 +109,6 @@ class _DesktopManagementStateNotifier extends ManagementStateNotifier {
|
||||
'new_lock_code': newLockCode,
|
||||
'reboot': reboot,
|
||||
});
|
||||
_ref.read(attachedDevicesProvider.notifier).refresh();
|
||||
ref.read(attachedDevicesProvider.notifier).refresh();
|
||||
}
|
||||
}
|
||||
|
@ -57,56 +57,48 @@ class _LockKeyNotifier extends StateNotifier<String?> {
|
||||
}
|
||||
}
|
||||
|
||||
final desktopOathState = StateNotifierProvider.autoDispose
|
||||
.family<OathStateNotifier, AsyncValue<OathState>, DevicePath>(
|
||||
(ref, devicePath) {
|
||||
final session = ref.watch(_sessionProvider(devicePath));
|
||||
final notifier = _DesktopOathStateNotifier(session, ref);
|
||||
session
|
||||
final desktopOathState = AsyncNotifierProvider.autoDispose
|
||||
.family<OathStateNotifier, OathState, DevicePath>(
|
||||
_DesktopOathStateNotifier.new);
|
||||
|
||||
class _DesktopOathStateNotifier extends OathStateNotifier {
|
||||
late RpcNodeSession _session;
|
||||
|
||||
@override
|
||||
FutureOr<OathState> build(DevicePath devicePath) async {
|
||||
_session = ref.watch(_sessionProvider(devicePath));
|
||||
_session
|
||||
..setErrorHandler('state-reset', (_) async {
|
||||
ref.invalidate(_sessionProvider(devicePath));
|
||||
})
|
||||
..setErrorHandler('auth-required', (_) async {
|
||||
await notifier.refresh();
|
||||
ref.invalidateSelf();
|
||||
});
|
||||
ref.onDispose(() {
|
||||
session
|
||||
_session
|
||||
..unsetErrorHandler('state-reset')
|
||||
..unsetErrorHandler('auth-required');
|
||||
});
|
||||
return notifier..refresh();
|
||||
},
|
||||
);
|
||||
|
||||
class _DesktopOathStateNotifier extends OathStateNotifier {
|
||||
final RpcNodeSession _session;
|
||||
final Ref _ref;
|
||||
_DesktopOathStateNotifier(this._session, this._ref) : super();
|
||||
|
||||
refresh() => updateState(() async {
|
||||
final result = await _session.command('get');
|
||||
_log.debug('application status', jsonEncode(result));
|
||||
var oathState = OathState.fromJson(result['data']);
|
||||
final key = _ref.read(_oathLockKeyProvider(_session.devicePath));
|
||||
if (oathState.locked && key != null) {
|
||||
final result =
|
||||
await _session.command('validate', params: {'key': key});
|
||||
if (result['valid']) {
|
||||
oathState = oathState.copyWith(locked: false);
|
||||
} else {
|
||||
_ref
|
||||
.read(_oathLockKeyProvider(_session.devicePath).notifier)
|
||||
.unsetKey();
|
||||
}
|
||||
}
|
||||
return oathState;
|
||||
});
|
||||
final result = await _session.command('get');
|
||||
_log.debug('application status', jsonEncode(result));
|
||||
var oathState = OathState.fromJson(result['data']);
|
||||
final key = ref.read(_oathLockKeyProvider(_session.devicePath));
|
||||
if (oathState.locked && key != null) {
|
||||
final result = await _session.command('validate', params: {'key': key});
|
||||
if (result['valid']) {
|
||||
oathState = oathState.copyWith(locked: false);
|
||||
} else {
|
||||
ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
}
|
||||
}
|
||||
return oathState;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reset() async {
|
||||
await _session.command('reset');
|
||||
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
_ref.invalidate(_sessionProvider(_session.devicePath));
|
||||
ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
ref.invalidate(_sessionProvider(_session.devicePath));
|
||||
}
|
||||
|
||||
@override
|
||||
@ -120,7 +112,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
|
||||
final bool remembered = validate['remembered'];
|
||||
if (valid) {
|
||||
_log.debug('applet unlocked');
|
||||
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).setKey(key);
|
||||
ref.read(_oathLockKeyProvider(_session.devicePath).notifier).setKey(key);
|
||||
setData(state.value!.copyWith(
|
||||
locked: false,
|
||||
remembered: remembered,
|
||||
@ -158,7 +150,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
|
||||
await _session.command('derive', params: {'password': password});
|
||||
var key = derive['key'];
|
||||
await _session.command('set_key', params: {'key': key});
|
||||
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).setKey(key);
|
||||
ref.read(_oathLockKeyProvider(_session.devicePath).notifier).setKey(key);
|
||||
}
|
||||
_log.debug('OATH key set');
|
||||
|
||||
@ -177,7 +169,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
|
||||
}
|
||||
}
|
||||
await _session.command('unset_key');
|
||||
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
setData(oathState.copyWith(hasKey: false, locked: false));
|
||||
return true;
|
||||
}
|
||||
@ -185,7 +177,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
|
||||
@override
|
||||
Future<void> forgetPassword() async {
|
||||
await _session.command('forget');
|
||||
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
setData(state.value!.copyWith(remembered: false));
|
||||
}
|
||||
}
|
||||
|
@ -21,9 +21,9 @@ import '../app/models.dart';
|
||||
import '../core/state.dart';
|
||||
import 'models.dart';
|
||||
|
||||
final fidoStateProvider = StateNotifierProvider.autoDispose
|
||||
.family<FidoStateNotifier, AsyncValue<FidoState>, DevicePath>(
|
||||
(ref, devicePath) => throw UnimplementedError(),
|
||||
final fidoStateProvider = AsyncNotifierProvider.autoDispose
|
||||
.family<FidoStateNotifier, FidoState, DevicePath>(
|
||||
() => throw UnimplementedError(),
|
||||
);
|
||||
|
||||
abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState> {
|
||||
|
@ -20,9 +20,9 @@ import 'package:yubico_authenticator/management/models.dart';
|
||||
import '../app/models.dart';
|
||||
import '../core/state.dart';
|
||||
|
||||
final managementStateProvider = StateNotifierProvider.autoDispose
|
||||
.family<ManagementStateNotifier, AsyncValue<DeviceInfo>, DevicePath>(
|
||||
(ref, devicePath) => throw UnimplementedError(),
|
||||
final managementStateProvider = AsyncNotifierProvider.autoDispose
|
||||
.family<ManagementStateNotifier, DeviceInfo, DevicePath>(
|
||||
() => throw UnimplementedError(),
|
||||
);
|
||||
|
||||
abstract class ManagementStateNotifier
|
||||
|
@ -37,9 +37,9 @@ class SearchNotifier extends StateNotifier<String> {
|
||||
}
|
||||
}
|
||||
|
||||
final oathStateProvider = StateNotifierProvider.autoDispose
|
||||
.family<OathStateNotifier, AsyncValue<OathState>, DevicePath>(
|
||||
(ref, devicePath) => throw UnimplementedError(),
|
||||
final oathStateProvider = AsyncNotifierProvider.autoDispose
|
||||
.family<OathStateNotifier, OathState, DevicePath>(
|
||||
() => throw UnimplementedError(),
|
||||
);
|
||||
|
||||
abstract class OathStateNotifier extends ApplicationStateNotifier<OathState> {
|
||||
|
@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -42,7 +42,6 @@ Widget oathBuildActions(
|
||||
}) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final capacity = oathState.version.isAtLeast(4) ? 32 : null;
|
||||
//final theme = Theme.of(context).colorScheme;
|
||||
final theme =
|
||||
ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme;
|
||||
return FsDialog(
|
||||
|
Loading…
Reference in New Issue
Block a user