mirror of
https://github.com/SyncrowIOT/syncrow-app.git
synced 2025-07-15 17:47:28 +00:00
garage Alarm
This commit is contained in:
@ -34,11 +34,14 @@ class GarageDoorBloc extends Bloc<GarageDoorEvent, GarageDoorSensorState> {
|
|||||||
on<ToggleDoorEvent>(openCloseGarageDoor);
|
on<ToggleDoorEvent>(openCloseGarageDoor);
|
||||||
on<GetCounterEvent>(_getCounterValue);
|
on<GetCounterEvent>(_getCounterValue);
|
||||||
on<SetCounterValue>(_setCounterValue);
|
on<SetCounterValue>(_setCounterValue);
|
||||||
|
on<SetTimeOutValue>(_setTimeOutAlarm);
|
||||||
on<TickTimer>(_onTickTimer);
|
on<TickTimer>(_onTickTimer);
|
||||||
on<InitialWizardEvent>(_fetchWizardStatus);
|
on<InitialWizardEvent>(_fetchWizardStatus);
|
||||||
on<GroupAllOnEvent>(_groupAllOn);
|
on<GroupAllOnEvent>(_groupAllOn);
|
||||||
on<GroupAllOffEvent>(_groupAllOff);
|
on<GroupAllOffEvent>(_groupAllOff);
|
||||||
on<ChangeFirstWizardSwitchStatusEvent>(_changeFirstWizardSwitch);
|
on<ChangeFirstWizardSwitchStatusEvent>(_changeFirstWizardSwitch);
|
||||||
|
on<ToggleAlarmEvent>(_toggleAlarmEvent);
|
||||||
|
//_toggleAlarmEvent
|
||||||
}
|
}
|
||||||
void _onClose(OnClose event, Emitter<GarageDoorSensorState> emit) {
|
void _onClose(OnClose event, Emitter<GarageDoorSensorState> emit) {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
@ -84,6 +87,32 @@ class GarageDoorBloc extends Bloc<GarageDoorEvent, GarageDoorSensorState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _toggleAlarmEvent(
|
||||||
|
ToggleAlarmEvent event,
|
||||||
|
Emitter<GarageDoorSensorState> emit,
|
||||||
|
) async {
|
||||||
|
emit(LoadingNewSate(doorSensor: deviceStatus));
|
||||||
|
|
||||||
|
try {
|
||||||
|
deviceStatus.doorState1 = event.isEnabled;
|
||||||
|
|
||||||
|
emit(UpdateState(garageSensor: deviceStatus));
|
||||||
|
|
||||||
|
await DevicesAPI.controlDevice(
|
||||||
|
DeviceControlModel(
|
||||||
|
deviceId: GDId,
|
||||||
|
code: 'door_state_1',
|
||||||
|
value: event.isEnabled,
|
||||||
|
),
|
||||||
|
GDId,
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(UpdateState(garageSensor: deviceStatus));
|
||||||
|
} catch (e) {
|
||||||
|
emit(GarageDoorFailedState(errorMessage: e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _toggleClosingReminder(ToggleClosingReminderEvent event,
|
void _toggleClosingReminder(ToggleClosingReminderEvent event,
|
||||||
Emitter<GarageDoorSensorState> emit) async {
|
Emitter<GarageDoorSensorState> emit) async {
|
||||||
emit(LoadingNewSate(doorSensor: deviceStatus));
|
emit(LoadingNewSate(doorSensor: deviceStatus));
|
||||||
@ -563,4 +592,29 @@ class GarageDoorBloc extends Bloc<GarageDoorEvent, GarageDoorSensorState> {
|
|||||||
add(InitialEvent(groupScreen: oneGangGroup));
|
add(InitialEvent(groupScreen: oneGangGroup));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setTimeOutAlarm(
|
||||||
|
SetTimeOutValue event, Emitter<GarageDoorSensorState> emit) async {
|
||||||
|
emit(LoadingNewSate(doorSensor: deviceStatus));
|
||||||
|
int seconds = 0;
|
||||||
|
try {
|
||||||
|
seconds = event.duration.inSeconds;
|
||||||
|
final response = await DevicesAPI.controlDevice(
|
||||||
|
DeviceControlModel(
|
||||||
|
deviceId: GDId, code: 'countdown_alarm', value: seconds),
|
||||||
|
GDId);
|
||||||
|
|
||||||
|
if (response['success'] ?? false) {
|
||||||
|
deviceStatus.countdownAlarm = seconds;
|
||||||
|
CustomSnackBar.displaySnackBar('Save Successfully');
|
||||||
|
add(GetScheduleEvent());
|
||||||
|
} else {
|
||||||
|
emit(const GarageDoorFailedState(errorMessage: 'Something went wrong'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
emit(GarageDoorFailedState(errorMessage: e.toString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,13 +39,13 @@ class GetCounterEvent extends GarageDoorEvent {
|
|||||||
List<Object> get props => [deviceCode];
|
List<Object> get props => [deviceCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
class ToggleLowBatteryEvent extends GarageDoorEvent {
|
class ToggleAlarmEvent extends GarageDoorEvent {
|
||||||
final bool isLowBatteryEnabled;
|
final String isEnabled;
|
||||||
|
|
||||||
const ToggleLowBatteryEvent(this.isLowBatteryEnabled);
|
const ToggleAlarmEvent(this.isEnabled);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [isLowBatteryEnabled];
|
List<Object> get props => [isEnabled];
|
||||||
}
|
}
|
||||||
|
|
||||||
class ToggleClosingReminderEvent extends GarageDoorEvent {
|
class ToggleClosingReminderEvent extends GarageDoorEvent {
|
||||||
@ -74,6 +74,14 @@ class SetCounterValue extends GarageDoorEvent {
|
|||||||
List<Object> get props => [duration, deviceCode];
|
List<Object> get props => [duration, deviceCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SetTimeOutValue extends GarageDoorEvent {
|
||||||
|
final Duration duration;
|
||||||
|
final String deviceCode;
|
||||||
|
const SetTimeOutValue({required this.duration, required this.deviceCode});
|
||||||
|
@override
|
||||||
|
List<Object> get props => [duration, deviceCode];
|
||||||
|
}
|
||||||
|
|
||||||
class StartTimer extends GarageDoorEvent {
|
class StartTimer extends GarageDoorEvent {
|
||||||
final int duration;
|
final int duration;
|
||||||
|
|
||||||
|
@ -21,7 +21,8 @@ class WallSensorBloc extends Bloc<WallSensorEvent, WallSensorState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _fetchCeilingSensorStatus(
|
void _fetchCeilingSensorStatus(
|
||||||
InitialEvent event, Emitter<WallSensorState> emit) async {
|
InitialEvent event,
|
||||||
|
Emitter<WallSensorState> emit) async {
|
||||||
emit(LoadingInitialState());
|
emit(LoadingInitialState());
|
||||||
try {
|
try {
|
||||||
var response = await DevicesAPI.getDeviceStatus(deviceId);
|
var response = await DevicesAPI.getDeviceStatus(deviceId);
|
||||||
|
@ -18,7 +18,8 @@ class PreferencesPage extends StatelessWidget {
|
|||||||
return DefaultScaffold(
|
return DefaultScaffold(
|
||||||
title: 'Preferences',
|
title: 'Preferences',
|
||||||
child: BlocProvider(
|
child: BlocProvider(
|
||||||
create: (context) => GarageDoorBloc(GDId: GDId)..add(const GarageDoorInitial()),
|
create: (context) =>
|
||||||
|
GarageDoorBloc(GDId: GDId)..add(const GarageDoorInitial()),
|
||||||
child: BlocBuilder<GarageDoorBloc, GarageDoorSensorState>(
|
child: BlocBuilder<GarageDoorBloc, GarageDoorSensorState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final garageDoorBloc = BlocProvider.of<GarageDoorBloc>(context);
|
final garageDoorBloc = BlocProvider.of<GarageDoorBloc>(context);
|
||||||
@ -26,7 +27,9 @@ class PreferencesPage extends StatelessWidget {
|
|||||||
return state is GarageDoorLoadingState
|
return state is GarageDoorLoadingState
|
||||||
? const Center(
|
? const Center(
|
||||||
child: DefaultContainer(
|
child: DefaultContainer(
|
||||||
width: 50, height: 50, child: CircularProgressIndicator()),
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
child: CircularProgressIndicator()),
|
||||||
)
|
)
|
||||||
: Column(
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
@ -49,14 +52,42 @@ class PreferencesPage extends StatelessWidget {
|
|||||||
height: 50,
|
height: 50,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
leading: const BodyMedium(
|
leading: InkWell(
|
||||||
text: 'Alarm when door is open',
|
onTap: () {
|
||||||
fontWeight: FontWeight.normal,
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return TimeoutDialog(
|
||||||
|
duration: Duration(
|
||||||
|
seconds: garageDoorBloc
|
||||||
|
.deviceStatus
|
||||||
|
.countdownAlarm),
|
||||||
|
title: 'Timeout Alarm',
|
||||||
|
cancelTab: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
confirmTab: (selectedSecond) {
|
||||||
|
garageDoorBloc.add(
|
||||||
|
SetTimeOutValue(
|
||||||
|
deviceCode:
|
||||||
|
'countdown_alarm',
|
||||||
|
duration:
|
||||||
|
selectedSecond));
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const BodyMedium(
|
||||||
|
text: 'Alarm when door is open',
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
trailing: Container(
|
trailing: Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
height: 30,
|
height: 30,
|
||||||
@ -64,18 +95,25 @@ class PreferencesPage extends StatelessWidget {
|
|||||||
color: ColorsManager.graysColor,
|
color: ColorsManager.graysColor,
|
||||||
),
|
),
|
||||||
Transform.scale(
|
Transform.scale(
|
||||||
scale: .8,
|
scale: .8,
|
||||||
child: CupertinoSwitch(
|
child: CupertinoSwitch(
|
||||||
value: garageDoorBloc.lowBattery,
|
value: garageDoorBloc.deviceStatus
|
||||||
onChanged: (value) {
|
.doorState1 ==
|
||||||
// context
|
'unclosed_time',
|
||||||
// .read<GarageDoorBloc>()
|
onChanged: (value) {
|
||||||
// .add(
|
context
|
||||||
// ToggleLowBatteryEvent(
|
.read<GarageDoorBloc>()
|
||||||
// value));
|
.add(
|
||||||
},
|
ToggleAlarmEvent(
|
||||||
applyTheme: true,
|
value
|
||||||
)),
|
? 'unclosed_time'
|
||||||
|
: 'close_time_alarm',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
applyTheme: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
@ -103,12 +141,15 @@ class PreferencesPage extends StatelessWidget {
|
|||||||
builder: (context) {
|
builder: (context) {
|
||||||
return SecondDialog(
|
return SecondDialog(
|
||||||
label2: 'Close',
|
label2: 'Close',
|
||||||
initialSelectedLabel: garageDoorBloc.secondSelected.toString(),
|
initialSelectedLabel: garageDoorBloc
|
||||||
|
.secondSelected
|
||||||
|
.toString(),
|
||||||
cancelTab: () {
|
cancelTab: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
confirmTab: (v) {
|
confirmTab: (v) {
|
||||||
garageDoorBloc.add(SelectSecondsEvent(seconds: v));
|
garageDoorBloc
|
||||||
|
.add(SelectSecondsEvent(seconds: v));
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
title: 'Control',
|
title: 'Control',
|
||||||
@ -129,8 +170,10 @@ class PreferencesPage extends StatelessWidget {
|
|||||||
height: 90,
|
height: 90,
|
||||||
width: 120,
|
width: 120,
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment:
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
BodyMedium(
|
BodyMedium(
|
||||||
fontColor: ColorsManager.textGray,
|
fontColor: ColorsManager.textGray,
|
||||||
@ -156,6 +199,7 @@ class PreferencesPage extends StatelessWidget {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//Timeout
|
||||||
|
|
||||||
class SecondDialog extends StatefulWidget {
|
class SecondDialog extends StatefulWidget {
|
||||||
final String label1;
|
final String label1;
|
||||||
@ -183,15 +227,12 @@ class SecondDialog extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SecondDialogState extends State<SecondDialog> {
|
class _SecondDialogState extends State<SecondDialog> {
|
||||||
// late String _selectedOption;
|
|
||||||
late int selectedSecond;
|
late int selectedSecond;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Parse the initialSelectedLabel as an integer. Default to 10 if invalid or not provided.
|
|
||||||
selectedSecond = int.tryParse(widget.initialSelectedLabel ?? '10') ?? 10;
|
selectedSecond = int.tryParse(widget.initialSelectedLabel ?? '10') ?? 10;
|
||||||
// _selectedOption = widget.initialSelectedLabel ?? '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -229,7 +270,6 @@ class _SecondDialogState extends State<SecondDialog> {
|
|||||||
child: CupertinoPicker(
|
child: CupertinoPicker(
|
||||||
itemExtent: 40.0,
|
itemExtent: 40.0,
|
||||||
scrollController: FixedExtentScrollController(
|
scrollController: FixedExtentScrollController(
|
||||||
// Set the initial position based on selectedSecond
|
|
||||||
initialItem: selectedSecond - 10,
|
initialItem: selectedSecond - 10,
|
||||||
),
|
),
|
||||||
onSelectedItemChanged: (int index) {
|
onSelectedItemChanged: (int index) {
|
||||||
@ -242,7 +282,9 @@ class _SecondDialogState extends State<SecondDialog> {
|
|||||||
child: BodyLarge(
|
child: BodyLarge(
|
||||||
text: (index + 10).toString().padLeft(2, '0'),
|
text: (index + 10).toString().padLeft(2, '0'),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w400, fontSize: 30, color: Colors.blue),
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 30,
|
||||||
|
color: Colors.blue),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@ -326,3 +368,153 @@ class _SecondDialogState extends State<SecondDialog> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TimeoutDialog extends StatefulWidget {
|
||||||
|
final String title;
|
||||||
|
final Function(String)? onTapLabel1;
|
||||||
|
final Function()? cancelTab;
|
||||||
|
final Function(Duration timeSelected)? confirmTab;
|
||||||
|
final Duration? duration;
|
||||||
|
|
||||||
|
TimeoutDialog({
|
||||||
|
required this.title,
|
||||||
|
this.onTapLabel1,
|
||||||
|
required this.cancelTab,
|
||||||
|
required this.confirmTab,
|
||||||
|
this.duration,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_TimeoutDialogState createState() => _TimeoutDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimeoutDialogState extends State<TimeoutDialog> {
|
||||||
|
late int selectedSecond;
|
||||||
|
Duration duration = Duration.zero;
|
||||||
|
int countNum = 0;
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
duration = widget.duration!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
BodyLarge(
|
||||||
|
text: widget.title,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontColor: ColorsManager.primaryColor,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(left: 15, right: 15),
|
||||||
|
child: Divider(
|
||||||
|
color: ColorsManager.textGray,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 15, right: 15, bottom: 10),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
height: 150,
|
||||||
|
width: MediaQuery.of(context).size.width * 1,
|
||||||
|
child: CupertinoTimerPicker(
|
||||||
|
initialTimerDuration: duration,
|
||||||
|
mode: CupertinoTimerPickerMode.hm,
|
||||||
|
onTimerDurationChanged: (Duration newDuration) {
|
||||||
|
duration = newDuration;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
right: BorderSide(
|
||||||
|
color: ColorsManager.textGray,
|
||||||
|
width: 0.5,
|
||||||
|
),
|
||||||
|
top: BorderSide(
|
||||||
|
color: ColorsManager.textGray,
|
||||||
|
width: 1.0,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
child: SizedBox(
|
||||||
|
child: InkWell(
|
||||||
|
onTap: widget.cancelTab,
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(15),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'Cancel',
|
||||||
|
style: TextStyle(
|
||||||
|
color: ColorsManager.textGray,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
left: BorderSide(
|
||||||
|
color: ColorsManager.textGray,
|
||||||
|
width: 0.5,
|
||||||
|
),
|
||||||
|
top: BorderSide(
|
||||||
|
color: ColorsManager.textGray,
|
||||||
|
width: 1.0,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
widget.confirmTab?.call(duration);
|
||||||
|
},
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(15),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'Confirm',
|
||||||
|
style: TextStyle(
|
||||||
|
color: ColorsManager.primaryColor,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDuration(int seconds) {
|
||||||
|
final hours = (seconds ~/ 3600).toString().padLeft(2, '0');
|
||||||
|
final minutes = ((seconds % 3600) ~/ 60).toString().padLeft(2, '0');
|
||||||
|
final secs = (seconds % 60).toString().padLeft(2, '0');
|
||||||
|
return '$hours:$minutes:$secs';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -38,6 +38,7 @@ class DevicesAPI {
|
|||||||
body: controlModel.toJson(),
|
body: controlModel.toJson(),
|
||||||
showServerMessage: true,
|
showServerMessage: true,
|
||||||
expectedResponseModel: (json) {
|
expectedResponseModel: (json) {
|
||||||
|
print(json);
|
||||||
return json;
|
return json;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -5,7 +5,7 @@ description: This is the mobile application project, developed with Flutter for
|
|||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
version: 1.0.3+26
|
version: 1.0.4+26
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.6 <4.0.0"
|
sdk: ">=3.0.6 <4.0.0"
|
||||||
|
Reference in New Issue
Block a user