1.优化注册切出去再回来问题
2.视频对讲优化 3.图传-视频对讲切出去再回来无法接听 4.优化远程开锁--使用蓝牙透传--判断蓝牙状态
This commit is contained in:
parent
d3969b6ba5
commit
b97d7006b3
@ -1,13 +1,8 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:star_lock/flavors.dart';
|
|
||||||
import 'package:star_lock/login/login/starLock_login_state.dart';
|
import 'package:star_lock/login/login/starLock_login_state.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/impl/udp_talk_ping_handler.dart';
|
|
||||||
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
|
||||||
import 'package:star_lock/tools/appFirstEnterHandle.dart';
|
import 'package:star_lock/tools/appFirstEnterHandle.dart';
|
||||||
import 'package:star_lock/tools/storage.dart';
|
import 'package:star_lock/tools/storage.dart';
|
||||||
import 'package:star_lock/tools/wechat/customer_tool.dart';
|
import 'package:star_lock/tools/wechat/customer_tool.dart';
|
||||||
@ -16,7 +11,6 @@ import '../../appRouters.dart';
|
|||||||
import '../../app_settings/app_colors.dart';
|
import '../../app_settings/app_colors.dart';
|
||||||
import '../../common/XSConstantMacro/XSConstantMacro.dart';
|
import '../../common/XSConstantMacro/XSConstantMacro.dart';
|
||||||
import '../../tools/commonItem.dart';
|
import '../../tools/commonItem.dart';
|
||||||
import '../../tools/jverify_one_click_login.dart';
|
|
||||||
import '../../tools/submitBtn.dart';
|
import '../../tools/submitBtn.dart';
|
||||||
import '../../tools/tf_loginInput.dart';
|
import '../../tools/tf_loginInput.dart';
|
||||||
import '../../tools/titleAppBar.dart';
|
import '../../tools/titleAppBar.dart';
|
||||||
@ -193,6 +187,7 @@ class _StarLockLoginPageState extends State<StarLockLoginPage> {
|
|||||||
inputFormatters: <TextInputFormatter>[
|
inputFormatters: <TextInputFormatter>[
|
||||||
LengthLimitingTextInputFormatter(20),
|
LengthLimitingTextInputFormatter(20),
|
||||||
]),
|
]),
|
||||||
|
_agreentWidget(),
|
||||||
SizedBox(height: 50.w),
|
SizedBox(height: 50.w),
|
||||||
Obx(() => SubmitBtn(
|
Obx(() => SubmitBtn(
|
||||||
btnName: '登录'.tr,
|
btnName: '登录'.tr,
|
||||||
@ -234,7 +229,7 @@ class _StarLockLoginPageState extends State<StarLockLoginPage> {
|
|||||||
|
|
||||||
Widget _agreentWidget() {
|
Widget _agreentWidget() {
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Obx(() => GestureDetector(
|
Obx(() => GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|||||||
@ -25,10 +25,10 @@ import 'starLock_register_state.dart';
|
|||||||
class StarLockRegisterLogic extends BaseGetXController {
|
class StarLockRegisterLogic extends BaseGetXController {
|
||||||
final StarLockRegisterState state = StarLockRegisterState();
|
final StarLockRegisterState state = StarLockRegisterState();
|
||||||
|
|
||||||
late Timer _timer;
|
Timer? _timer;
|
||||||
|
|
||||||
void _startTimer() {
|
void _startTimer() {
|
||||||
_timer = Timer.periodic(1.seconds, (Timer timer) {
|
_timer = Timer.periodic(const Duration(seconds: 1), (Timer timer) {
|
||||||
if (state.currentSecond > 1) {
|
if (state.currentSecond > 1) {
|
||||||
state.currentSecond--;
|
state.currentSecond--;
|
||||||
} else {
|
} else {
|
||||||
@ -40,7 +40,8 @@ class StarLockRegisterLogic extends BaseGetXController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _cancelTimer() {
|
void _cancelTimer() {
|
||||||
_timer.cancel();
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
// _timer = null;
|
// _timer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,4 +163,27 @@ class StarLockRegisterLogic extends BaseGetXController {
|
|||||||
|
|
||||||
await checkIpAction();
|
await checkIpAction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearInputData() {
|
||||||
|
// 清除手机号/邮箱输入框
|
||||||
|
state.phoneOrEmailController.clear();
|
||||||
|
state.phoneOrEmailStr.value = '';
|
||||||
|
|
||||||
|
// 清除密码输入框
|
||||||
|
state.pwdController.clear();
|
||||||
|
state.pwd.value = '';
|
||||||
|
|
||||||
|
// 清除确认密码输入框
|
||||||
|
state.sureController.clear();
|
||||||
|
state.surePwd.value = '';
|
||||||
|
|
||||||
|
// 清除验证码输入框
|
||||||
|
state.codeController.clear();
|
||||||
|
state.verificationCode.value = '';
|
||||||
|
|
||||||
|
// 安全地重置验证码倒计时
|
||||||
|
_cancelTimer();
|
||||||
|
state.currentSecond = state.totalSeconds;
|
||||||
|
state.resetResend();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -89,7 +89,12 @@ class _StarLockRegisterPageState extends State<StarLockRegisterPage> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
// 切换到手机号输入前先清除数据
|
||||||
|
if (!state.isIphoneType.value) {
|
||||||
|
logic.clearInputData();
|
||||||
|
}
|
||||||
state.isIphoneType.value = true;
|
state.isIphoneType.value = true;
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
},
|
},
|
||||||
child: Obx(
|
child: Obx(
|
||||||
() => Container(
|
() => Container(
|
||||||
@ -113,6 +118,11 @@ class _StarLockRegisterPageState extends State<StarLockRegisterPage> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
// 切换到邮箱输入前先清除数据
|
||||||
|
if (state.isIphoneType.value) {
|
||||||
|
logic.clearInputData();
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
}
|
||||||
state.isIphoneType.value = false;
|
state.isIphoneType.value = false;
|
||||||
},
|
},
|
||||||
child: Obx(
|
child: Obx(
|
||||||
|
|||||||
@ -696,6 +696,18 @@ class LockDetailLogic extends BaseGetXController {
|
|||||||
|
|
||||||
final lockPeerId = StartChartManage().lockPeerId;
|
final lockPeerId = StartChartManage().lockPeerId;
|
||||||
final LockListInfoGroupEntity? lockListInfoGroupEntity = await Storage.getLockMainListData();
|
final LockListInfoGroupEntity? lockListInfoGroupEntity = await Storage.getLockMainListData();
|
||||||
|
|
||||||
|
//检查蓝牙连接状态
|
||||||
|
final connectedDevices = await FlutterBluePlus.connectedDevices;
|
||||||
|
final isDeviceConnected = connectedDevices.any((device) =>
|
||||||
|
device.remoteId.str == BlueManage().connectDeviceName);
|
||||||
|
|
||||||
|
if (!isDeviceConnected) {
|
||||||
|
AppLog.log('蓝牙已断开,尝试重新连接');
|
||||||
|
// 蓝牙断开,需要重新连接并获取新的token
|
||||||
|
await _reconnectAndRefreshToken();
|
||||||
|
}
|
||||||
|
|
||||||
if (lockListInfoGroupEntity != null) {
|
if (lockListInfoGroupEntity != null) {
|
||||||
lockListInfoGroupEntity!.groupList?.forEach((element) {
|
lockListInfoGroupEntity!.groupList?.forEach((element) {
|
||||||
final lockList = element.lockList;
|
final lockList = element.lockList;
|
||||||
@ -712,7 +724,11 @@ class LockDetailLogic extends BaseGetXController {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remoteUnlock == 1) {
|
if (remoteUnlock == 1) {
|
||||||
|
// 发送蓝牙透传开锁
|
||||||
|
await _sendUnlockViaBluetooth();
|
||||||
|
// 发送远程开锁api
|
||||||
final LoginEntity entity = await ApiRepository.to.remoteOpenLock(lockId: lockId.toString(), timeOut: 60);
|
final LoginEntity entity = await ApiRepository.to.remoteOpenLock(lockId: lockId.toString(), timeOut: 60);
|
||||||
if (entity.errorCode!.codeIsSuccessful) {
|
if (entity.errorCode!.codeIsSuccessful) {
|
||||||
showToast('已开锁'.tr);
|
showToast('已开锁'.tr);
|
||||||
@ -721,6 +737,179 @@ class LockDetailLogic extends BaseGetXController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 新增方法:通过蓝牙透传发送开锁命令
|
||||||
|
Future<void> _sendUnlockViaBluetooth() async {
|
||||||
|
try {
|
||||||
|
// 获取必要的密钥信息
|
||||||
|
final List<String>? privateKey = await Storage.getStringList(saveBluePrivateKey);
|
||||||
|
final List<int> privateKeyList = changeStringListToIntList(privateKey!);
|
||||||
|
|
||||||
|
final List<String>? signKey = await Storage.getStringList(saveBlueSignKey);
|
||||||
|
final List<int> signKeyList = changeStringListToIntList(signKey!);
|
||||||
|
|
||||||
|
final List<String>? token = await Storage.getStringList(saveBlueToken);
|
||||||
|
final List<int> tokenList = changeStringListToIntList(token!);
|
||||||
|
|
||||||
|
// 构建开锁命令
|
||||||
|
final OpenLockCommand openLockCommand = OpenLockCommand(
|
||||||
|
lockID: BlueManage().connectDeviceName,
|
||||||
|
userID: await Storage.getUid(),
|
||||||
|
openMode: state.openDoorModel,
|
||||||
|
openTime: getUTCNetTime(),
|
||||||
|
onlineToken: state.lockNetToken,
|
||||||
|
token: tokenList,
|
||||||
|
needAuthor: 1,
|
||||||
|
signKey: signKeyList,
|
||||||
|
privateKey: privateKeyList,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 包装命令数据
|
||||||
|
final List<int> messageDetail = openLockCommand.packageData();
|
||||||
|
|
||||||
|
// 通过蓝牙透传发送开锁命令
|
||||||
|
StartChartManage().sendRemoteUnLockMessage(
|
||||||
|
bluetoothDeviceName: BlueManage().connectDeviceName,
|
||||||
|
openLockCommand: messageDetail,
|
||||||
|
);
|
||||||
|
|
||||||
|
AppLog.log('通过蓝牙透传发送远程开锁命令');
|
||||||
|
|
||||||
|
// 监听开锁结果,处理token不一致的情况
|
||||||
|
_listenForOpenLockReply(tokenList);
|
||||||
|
} catch (e) {
|
||||||
|
AppLog.log('蓝牙透传开锁异常: $e');
|
||||||
|
showToast('蓝牙透传开锁失败'.tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听开锁回复,处理token验证
|
||||||
|
void _listenForOpenLockReply(List<int> originalToken) {
|
||||||
|
StreamSubscription<Reply>? subscription;
|
||||||
|
// 设置监听器处理开锁回复
|
||||||
|
subscription = EventBusManager().eventBus!.on<Reply>().listen((Reply reply) async {
|
||||||
|
if (reply is OpenDoorReply) {
|
||||||
|
final int status = reply.data[6];
|
||||||
|
|
||||||
|
// 如果是token不一致的情况(状态码0x06)
|
||||||
|
if (status == 0x06) {
|
||||||
|
AppLog.log('token不一致,使用新token重新开锁');
|
||||||
|
|
||||||
|
// 提取新的token
|
||||||
|
final List<int> newToken = reply.data.sublist(2, 6);
|
||||||
|
|
||||||
|
// 保存新token到存储
|
||||||
|
final List<String> saveStrList = changeIntListToStringList(newToken);
|
||||||
|
Storage.setStringList(saveBlueToken, saveStrList);
|
||||||
|
|
||||||
|
// 使用新token重新发送开锁命令
|
||||||
|
await _reSendUnlockWithNewToken(newToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消监听
|
||||||
|
subscription?.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置超时机制,避免监听器一直存在
|
||||||
|
Timer(const Duration(seconds: 10), () {
|
||||||
|
subscription?.cancel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用新token重新发送开锁命令
|
||||||
|
Future<void> _reSendUnlockWithNewToken(List<int> newToken) async {
|
||||||
|
try {
|
||||||
|
// 获取必要的密钥信息
|
||||||
|
final List<String>? privateKey = await Storage.getStringList(saveBluePrivateKey);
|
||||||
|
final List<int> privateKeyList = changeStringListToIntList(privateKey!);
|
||||||
|
|
||||||
|
final List<String>? signKey = await Storage.getStringList(saveBlueSignKey);
|
||||||
|
final List<int> signKeyList = changeStringListToIntList(signKey!);
|
||||||
|
|
||||||
|
// 构建新的开锁命令
|
||||||
|
final OpenLockCommand openLockCommand = OpenLockCommand(
|
||||||
|
lockID: BlueManage().connectDeviceName,
|
||||||
|
userID: await Storage.getUid(),
|
||||||
|
openMode: state.openDoorModel,
|
||||||
|
openTime: getUTCNetTime(),
|
||||||
|
onlineToken: state.lockNetToken,
|
||||||
|
token: newToken,
|
||||||
|
needAuthor: 1,
|
||||||
|
signKey: signKeyList,
|
||||||
|
privateKey: privateKeyList,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 包装命令数据
|
||||||
|
final List<int> messageDetail = openLockCommand.packageData();
|
||||||
|
|
||||||
|
// 通过蓝牙透传发送开锁命令
|
||||||
|
StartChartManage().sendRemoteUnLockMessage(
|
||||||
|
bluetoothDeviceName: BlueManage().connectDeviceName,
|
||||||
|
openLockCommand: messageDetail,
|
||||||
|
);
|
||||||
|
|
||||||
|
AppLog.log('使用新token重新发送开锁命令');
|
||||||
|
} catch (e) {
|
||||||
|
AppLog.log('使用新token重新开锁异常: $e');
|
||||||
|
showToast('重新开锁失败'.tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增方法:重新连接蓝牙并刷新token
|
||||||
|
Future<void> _reconnectAndRefreshToken() async {
|
||||||
|
try {
|
||||||
|
// 重新连接蓝牙
|
||||||
|
await BlueManage().blueSendData(
|
||||||
|
state.keyInfos.value.bluetooth!.bluetoothDeviceName!,
|
||||||
|
(BluetoothConnectionState deviceConnectionState) async {
|
||||||
|
if (deviceConnectionState == BluetoothConnectionState.connected) {
|
||||||
|
AppLog.log('蓝牙重新连接成功');
|
||||||
|
// 获取新的token
|
||||||
|
await _refreshLockToken();
|
||||||
|
} else if (deviceConnectionState == BluetoothConnectionState.disconnected) {
|
||||||
|
AppLog.log('蓝牙重新连接失败');
|
||||||
|
showToast('蓝牙连接失败,请重试'.tr);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
AppLog.log('蓝牙重连异常: $e');
|
||||||
|
showToast('蓝牙连接异常,请重试'.tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增方法:刷新锁token
|
||||||
|
Future<void> _refreshLockToken() async {
|
||||||
|
try {
|
||||||
|
final List<String>? token = await Storage.getStringList(saveBlueToken);
|
||||||
|
final List<int> tokenList = changeStringListToIntList(token!);
|
||||||
|
|
||||||
|
final List<String>? privateKey = await Storage.getStringList(saveBluePrivateKey);
|
||||||
|
final List<int> privateKeyList = changeStringListToIntList(privateKey!);
|
||||||
|
|
||||||
|
final List<String>? signKey = await Storage.getStringList(saveBlueSignKey);
|
||||||
|
final List<int> signKeyList = changeStringListToIntList(signKey!);
|
||||||
|
|
||||||
|
// 发送获取新token的命令
|
||||||
|
IoSenderManage.senderOpenLock(
|
||||||
|
lockID: BlueManage().connectDeviceName,
|
||||||
|
userID: await Storage.getUid(),
|
||||||
|
openMode: state.openDoorModel,
|
||||||
|
openTime: getUTCNetTime(),
|
||||||
|
onlineToken: state.lockNetToken,
|
||||||
|
token: tokenList,
|
||||||
|
needAuthor: 1,
|
||||||
|
signKey: signKeyList,
|
||||||
|
privateKey: privateKeyList,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 等待一段时间让token更新完成
|
||||||
|
await Future.delayed(Duration(milliseconds: 500));
|
||||||
|
} catch (e) {
|
||||||
|
AppLog.log('刷新token异常: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 锁设置里面开启关闭考勤刷新锁详情
|
/// 锁设置里面开启关闭考勤刷新锁详情
|
||||||
void initLockSetOpenOrCloseCheckInRefreshLockDetailWithAttendanceAction() {
|
void initLockSetOpenOrCloseCheckInRefreshLockDetailWithAttendanceAction() {
|
||||||
// 蓝牙协议通知传输跟蓝牙之外的数据传输类不一样 eventBus
|
// 蓝牙协议通知传输跟蓝牙之外的数据传输类不一样 eventBus
|
||||||
|
|||||||
@ -161,6 +161,18 @@ class StartChartManage {
|
|||||||
await reportInformation();
|
await reportInformation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 优化连接建立
|
||||||
|
Future<void> establishConnection({required String ToPeerId}) async {
|
||||||
|
// 提前预加载必要资源
|
||||||
|
await init(); // 确保基础服务已初始化
|
||||||
|
|
||||||
|
// 并行执行地址交换和打洞
|
||||||
|
await Future.wait([
|
||||||
|
startSendingRbcuInfoMessages(ToPeerId: ToPeerId),
|
||||||
|
startSendingRbcuProbeTMessages(),
|
||||||
|
] as Iterable<Future>);
|
||||||
|
}
|
||||||
|
|
||||||
/// 客户端注册
|
/// 客户端注册
|
||||||
Future<void> _clientRegister(LoginData? loginData) async {
|
Future<void> _clientRegister(LoginData? loginData) async {
|
||||||
if (loginData?.starchart?.starchartId != null) {
|
if (loginData?.starchart?.starchartId != null) {
|
||||||
@ -457,7 +469,7 @@ class StartChartManage {
|
|||||||
// 启动定时器持续发送对讲请求
|
// 启动定时器持续发送对讲请求
|
||||||
talkRequestTimer ??= Timer.periodic(
|
talkRequestTimer ??= Timer.periodic(
|
||||||
Duration(
|
Duration(
|
||||||
seconds: _defaultIntervalTime,
|
milliseconds: 500,
|
||||||
),
|
),
|
||||||
(Timer timer) async {
|
(Timer timer) async {
|
||||||
AppLog.log('发送对讲请求:${ToPeerId}');
|
AppLog.log('发送对讲请求:${ToPeerId}');
|
||||||
@ -719,7 +731,14 @@ class StartChartManage {
|
|||||||
FromPeerId: FromPeerId,
|
FromPeerId: FromPeerId,
|
||||||
MessageId: MessageCommand.getNextMessageId(ToPeerId, increment: true),
|
MessageId: MessageCommand.getNextMessageId(ToPeerId, increment: true),
|
||||||
);
|
);
|
||||||
await _sendMessage(message: message);
|
try {
|
||||||
|
await _sendMessage(message: message);
|
||||||
|
} catch (e) {
|
||||||
|
// 记录日志但不中断程序执行
|
||||||
|
AppLog.log('发送挂断消息失败: $e');
|
||||||
|
// 不抛出异常,避免应用崩溃
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送通话中挂断消息
|
// 发送通话中挂断消息
|
||||||
@ -800,9 +819,16 @@ class StartChartManage {
|
|||||||
// 发送消息
|
// 发送消息
|
||||||
Future<void> _sendMessage({required List<int> message}) async {
|
Future<void> _sendMessage({required List<int> message}) async {
|
||||||
var result = await _udpSocket?.send(message, InternetAddress(remoteHost), remotePort);
|
var result = await _udpSocket?.send(message, InternetAddress(remoteHost), remotePort);
|
||||||
if (result != message.length) {
|
// 在对讲管理类中完善异常处理
|
||||||
throw StartChartMessageException('❌Udp send data error----> $result ${message.length}');
|
try {
|
||||||
// _udpSocket = null;
|
// UDP发送逻辑
|
||||||
|
} catch (e) {
|
||||||
|
// 记录错误但不影响主流程
|
||||||
|
if (result != message.length) {
|
||||||
|
throw StartChartMessageException('❌Udp send data error----> $result ${message.length}');
|
||||||
|
// _udpSocket = null;
|
||||||
|
}
|
||||||
|
print('对讲消息发送失败: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
//ToDo: 增加对讲调试、正式可删除
|
//ToDo: 增加对讲调试、正式可删除
|
||||||
|
|||||||
@ -68,9 +68,10 @@ class AppLifecycleObserver extends WidgetsBindingObserver {
|
|||||||
status == TalkStatus.proactivelyCallWaitingAnswer ||
|
status == TalkStatus.proactivelyCallWaitingAnswer ||
|
||||||
status == TalkStatus.answeredSuccessfully ||
|
status == TalkStatus.answeredSuccessfully ||
|
||||||
status == TalkStatus.uninitialized) {
|
status == TalkStatus.uninitialized) {
|
||||||
Get.back();
|
// Get.back(); // 避免返回上一页的操作,避免影响注册页
|
||||||
}
|
}
|
||||||
StartChartManage().destruction();
|
// 不进行对讲资源清理,避免切出去回来无法接听
|
||||||
|
// StartChartManage().destruction();
|
||||||
_readMessageRefreshUIEvent?.cancel();
|
_readMessageRefreshUIEvent?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user