mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-10 07:07:19 +00:00
Sp 1711 fe implement blank state (#288)
<!-- Thanks for contributing! Provide a description of your changes below and a general summary in the title Please look at the following checklist to ensure that your PR can be accepted quickly: --> ## Jira Ticket [SP-1711](https://syncrow.atlassian.net/browse/SP-1711) [SP-1712](https://syncrow.atlassian.net/browse/SP-1712) ## Description 1. Shows tooltip on space cell. 2. navigates to space when a new space is selected 3. Fixed a thrown Exception because of an Expanded widget 4. Adjusted connections between spaces to select from and to nodes. 5. Created a `SpaceDetailsDialog` and a helper class to show it, because a space can be created through different widgets, and this was done to unify the logic and remove code duplication. ## Type of Change <!--- Put an `x` in all the boxes that apply: --> - [x] ✨ New feature (non-breaking change which adds functionality) - [x] 🛠️ Bug fix (non-breaking change which fixes an issue) - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) - [ ] 🧹 Code refactor - [ ] ✅ Build configuration change - [ ] 📝 Documentation - [ ] 🗑️ Chore [SP-1711]: https://syncrow.atlassian.net/browse/SP-1711?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [SP-1712]: https://syncrow.atlassian.net/browse/SP-1712?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
This commit is contained in:
@ -7,21 +7,22 @@ class SpacesConnectionsArrowPainter extends CustomPainter {
|
||||
final Map<String, Offset> positions;
|
||||
final double cardWidth = 150.0;
|
||||
final double cardHeight = 90.0;
|
||||
final String? selectedSpaceUuid;
|
||||
final Set<String> highlightedUuids;
|
||||
|
||||
SpacesConnectionsArrowPainter({
|
||||
required this.connections,
|
||||
required this.positions,
|
||||
this.selectedSpaceUuid,
|
||||
required this.highlightedUuids,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
for (final connection in connections) {
|
||||
final isSelected = connection.to == selectedSpaceUuid;
|
||||
final isSelected = highlightedUuids.contains(connection.from) ||
|
||||
highlightedUuids.contains(connection.to);
|
||||
final paint = Paint()
|
||||
..color = isSelected
|
||||
? ColorsManager.primaryColor
|
||||
? ColorsManager.blackColor
|
||||
: ColorsManager.blackColor.withValues(alpha: 0.5)
|
||||
..strokeWidth = 2.0
|
||||
..style = PaintingStyle.stroke;
|
||||
@ -36,7 +37,7 @@ class SpacesConnectionsArrowPainter extends CustomPainter {
|
||||
|
||||
final path = Path()..moveTo(startPoint.dx, startPoint.dy);
|
||||
|
||||
final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 60);
|
||||
final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 20);
|
||||
final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60);
|
||||
|
||||
path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx,
|
||||
@ -46,7 +47,7 @@ class SpacesConnectionsArrowPainter extends CustomPainter {
|
||||
|
||||
final circlePaint = Paint()
|
||||
..color = isSelected
|
||||
? ColorsManager.primaryColor
|
||||
? ColorsManager.blackColor
|
||||
: ColorsManager.blackColor.withValues(alpha: 0.5)
|
||||
..style = PaintingStyle.fill
|
||||
..blendMode = BlendMode.srcIn;
|
||||
|
@ -1,18 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||
|
||||
class CommunityStructureCanvas extends StatefulWidget {
|
||||
const CommunityStructureCanvas({
|
||||
required this.community,
|
||||
required this.selectedSpace,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final CommunityModel community;
|
||||
final SpaceModel? selectedSpace;
|
||||
|
||||
@override
|
||||
State<CommunityStructureCanvas> createState() => _CommunityStructureCanvasState();
|
||||
@ -25,19 +30,30 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
final double _cardHeight = 90.0;
|
||||
final double _horizontalSpacing = 150.0;
|
||||
final double _verticalSpacing = 120.0;
|
||||
String? _selectedSpaceUuid;
|
||||
|
||||
late TransformationController _transformationController;
|
||||
late AnimationController _animationController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_transformationController = TransformationController();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
duration: const Duration(milliseconds: 150),
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_animateToSpace(widget.selectedSpace);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -47,6 +63,15 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Set<String> _getAllDescendantUuids(SpaceModel space) {
|
||||
final uuids = <String>{};
|
||||
for (final child in space.children) {
|
||||
uuids.add(child.uuid);
|
||||
uuids.addAll(_getAllDescendantUuids(child));
|
||||
}
|
||||
return uuids;
|
||||
}
|
||||
|
||||
void _runAnimation(Matrix4 target) {
|
||||
final animation = Matrix4Tween(
|
||||
begin: _transformationController.value,
|
||||
@ -63,15 +88,16 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
});
|
||||
}
|
||||
|
||||
void _onSpaceTapped(String spaceUuid) {
|
||||
setState(() {
|
||||
_selectedSpaceUuid = spaceUuid;
|
||||
});
|
||||
void _animateToSpace(SpaceModel? space) {
|
||||
if (space == null) {
|
||||
_runAnimation(Matrix4.identity());
|
||||
return;
|
||||
}
|
||||
|
||||
final position = _positions[spaceUuid];
|
||||
final position = _positions[space.uuid];
|
||||
if (position == null) return;
|
||||
|
||||
const scale = 2.0;
|
||||
const scale = 1.5;
|
||||
final viewSize = context.size;
|
||||
if (viewSize == null) return;
|
||||
|
||||
@ -86,11 +112,19 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
_runAnimation(matrix);
|
||||
}
|
||||
|
||||
void _onSpaceTapped(SpaceModel? space) {
|
||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||
SelectSpaceEvent(community: widget.community, space: space),
|
||||
);
|
||||
}
|
||||
|
||||
void _resetSelectionAndZoom() {
|
||||
setState(() {
|
||||
_selectedSpaceUuid = null;
|
||||
});
|
||||
_runAnimation(Matrix4.identity());
|
||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||
SelectSpaceEvent(
|
||||
community: widget.community,
|
||||
space: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _calculateLayout(
|
||||
@ -150,16 +184,23 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
|
||||
_calculateLayout(community.spaces, 0, {});
|
||||
|
||||
final selectedSpace = widget.selectedSpace;
|
||||
final highlightedUuids = <String>{};
|
||||
if (selectedSpace != null) {
|
||||
highlightedUuids.add(selectedSpace.uuid);
|
||||
highlightedUuids.addAll(_getAllDescendantUuids(selectedSpace));
|
||||
}
|
||||
|
||||
final widgets = <Widget>[];
|
||||
final connections = <SpaceConnectionModel>[];
|
||||
_generateWidgets(community.spaces, widgets, connections);
|
||||
_generateWidgets(community.spaces, widgets, connections, highlightedUuids);
|
||||
|
||||
return [
|
||||
CustomPaint(
|
||||
painter: SpacesConnectionsArrowPainter(
|
||||
connections: connections,
|
||||
positions: _positions,
|
||||
selectedSpaceUuid: _selectedSpaceUuid,
|
||||
highlightedUuids: highlightedUuids,
|
||||
),
|
||||
child: Stack(alignment: AlignmentDirectional.center, children: widgets),
|
||||
),
|
||||
@ -170,11 +211,15 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
List<SpaceModel> spaces,
|
||||
List<Widget> widgets,
|
||||
List<SpaceConnectionModel> connections,
|
||||
Set<String> highlightedUuids,
|
||||
) {
|
||||
for (final space in spaces) {
|
||||
final position = _positions[space.uuid];
|
||||
if (position == null) continue;
|
||||
|
||||
final isHighlighted = highlightedUuids.contains(space.uuid);
|
||||
final hasNoSelectedSpace = widget.selectedSpace == null;
|
||||
|
||||
widgets.add(
|
||||
Positioned(
|
||||
left: position.dx,
|
||||
@ -182,32 +227,31 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
width: _cardWidth,
|
||||
height: _cardHeight,
|
||||
child: SpaceCardWidget(
|
||||
index: spaces.indexOf(space),
|
||||
onPositionChanged: (newPosition) {},
|
||||
buildSpaceContainer: (index) {
|
||||
buildSpaceContainer: () {
|
||||
return Opacity(
|
||||
opacity: 1.0,
|
||||
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
|
||||
child: Tooltip(
|
||||
message: space.spaceName,
|
||||
preferBelow: false,
|
||||
child: SpaceCell(
|
||||
index: index,
|
||||
onTap: () => _onSpaceTapped(space.uuid),
|
||||
onTap: () => _onSpaceTapped(space),
|
||||
icon: space.icon,
|
||||
name: space.spaceName,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
screenSize: MediaQuery.sizeOf(context),
|
||||
position: position,
|
||||
isHovered: false,
|
||||
onHoverChanged: (int index, bool isHovered) {},
|
||||
onButtonTap: (int index, Offset newPosition) {},
|
||||
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
for (final child in space.children) {
|
||||
connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid));
|
||||
connections.add(
|
||||
SpaceConnectionModel(from: space.uuid, to: child.uuid),
|
||||
);
|
||||
}
|
||||
_generateWidgets(space.children, widgets, connections);
|
||||
_generateWidgets(space.children, widgets, connections, highlightedUuids);
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,7 +262,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
transformationController: _transformationController,
|
||||
boundaryMargin: EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.sizeOf(context).width * 0.3,
|
||||
vertical: MediaQuery.sizeOf(context).height * 0.2,
|
||||
vertical: MediaQuery.sizeOf(context).height * 0.3,
|
||||
),
|
||||
minScale: 0.5,
|
||||
maxScale: 3.0,
|
||||
@ -226,8 +270,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
child: GestureDetector(
|
||||
onTap: _resetSelectionAndZoom,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 2,
|
||||
height: MediaQuery.sizeOf(context).height * 2,
|
||||
width: MediaQuery.sizeOf(context).width * 5,
|
||||
height: MediaQuery.sizeOf(context).height * 5,
|
||||
child: Stack(children: treeWidgets),
|
||||
),
|
||||
),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CreateSpaceButton extends StatelessWidget {
|
||||
@ -7,7 +8,7 @@ class CreateSpaceButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {},
|
||||
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
|
||||
child: Container(
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
|
@ -2,15 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class PlusButtonWidget extends StatelessWidget {
|
||||
final int index;
|
||||
final String direction;
|
||||
final Offset offset;
|
||||
final void Function(int index, Offset newPosition) onButtonTap;
|
||||
final void Function() onButtonTap;
|
||||
|
||||
const PlusButtonWidget({
|
||||
super.key,
|
||||
required this.index,
|
||||
required this.direction,
|
||||
required this.offset,
|
||||
required this.onButtonTap,
|
||||
});
|
||||
@ -18,13 +14,7 @@ class PlusButtonWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (direction == 'down') {
|
||||
onButtonTap(index, const Offset(0, 150));
|
||||
} else {
|
||||
onButtonTap(index, const Offset(150, 0));
|
||||
}
|
||||
},
|
||||
onTap: onButtonTap,
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
|
@ -1,60 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/plus_button_widget.dart';
|
||||
|
||||
class SpaceCardWidget extends StatelessWidget {
|
||||
final int index;
|
||||
final Size screenSize;
|
||||
final Offset position;
|
||||
final bool isHovered;
|
||||
final void Function(int index, bool isHovered) onHoverChanged;
|
||||
final void Function(int index, Offset newPosition) onButtonTap;
|
||||
final Widget Function(int index) buildSpaceContainer;
|
||||
final ValueChanged<Offset> onPositionChanged;
|
||||
class SpaceCardWidget extends StatefulWidget {
|
||||
final void Function() onTap;
|
||||
final Widget Function() buildSpaceContainer;
|
||||
|
||||
const SpaceCardWidget({
|
||||
super.key,
|
||||
required this.index,
|
||||
required this.onPositionChanged,
|
||||
required this.screenSize,
|
||||
required this.position,
|
||||
required this.isHovered,
|
||||
required this.onHoverChanged,
|
||||
required this.onButtonTap,
|
||||
required this.onTap,
|
||||
required this.buildSpaceContainer,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SpaceCardWidget> createState() => _SpaceCardWidgetState();
|
||||
}
|
||||
|
||||
class _SpaceCardWidgetState extends State<SpaceCardWidget> {
|
||||
bool isHovered = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => onHoverChanged(index, true),
|
||||
onExit: (_) => onHoverChanged(index, false),
|
||||
onEnter: (_) => setState(() => isHovered = true),
|
||||
onExit: (_) => setState(() => isHovered = false),
|
||||
child: SizedBox(
|
||||
width: 150,
|
||||
height: 90,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
buildSpaceContainer(index),
|
||||
|
||||
widget.buildSpaceContainer(),
|
||||
if (isHovered)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
child: PlusButtonWidget(
|
||||
index: index,
|
||||
direction: 'down',
|
||||
offset: Offset.zero,
|
||||
onButtonTap: onButtonTap,
|
||||
),
|
||||
),
|
||||
if (isHovered)
|
||||
Positioned(
|
||||
right: -15,
|
||||
child: PlusButtonWidget(
|
||||
index: index,
|
||||
direction: 'right',
|
||||
offset: Offset.zero,
|
||||
onButtonTap: onButtonTap,
|
||||
onButtonTap: widget.onTap,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -1,29 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class SpaceCell extends StatelessWidget {
|
||||
final int index;
|
||||
final String icon;
|
||||
final String name;
|
||||
final VoidCallback? onDoubleTap;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const SpaceCell({
|
||||
super.key,
|
||||
required this.index,
|
||||
required this.icon,
|
||||
required this.name,
|
||||
this.onTap,
|
||||
this.onDoubleTap,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return GestureDetector(
|
||||
onDoubleTap: onDoubleTap,
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 150,
|
||||
@ -36,7 +30,7 @@ class SpaceCell extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
@ -63,7 +57,10 @@ class SpaceCell extends StatelessWidget {
|
||||
child: Center(
|
||||
child: SvgPicture.asset(
|
||||
icon,
|
||||
color: ColorsManager.whiteColors,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
ColorsManager.whiteColors,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
|
@ -9,14 +9,19 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedCommunity =
|
||||
context.watch<CommunitiesTreeSelectionBloc>().state.selectedCommunity!;
|
||||
final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state;
|
||||
final selectedCommunity = selectionBloc.selectedCommunity;
|
||||
final selectedSpace = selectionBloc.selectedSpace;
|
||||
const spacer = Spacer(flex: 10);
|
||||
return Visibility(
|
||||
visible: selectedCommunity.spaces.isNotEmpty,
|
||||
visible: selectedCommunity!.spaces.isNotEmpty,
|
||||
replacement: const Row(
|
||||
children: [spacer, Expanded(child: CreateSpaceButton()), spacer]),
|
||||
child: CommunityStructureCanvas(community: selectedCommunity),
|
||||
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
|
||||
),
|
||||
child: CommunityStructureCanvas(
|
||||
community: selectedCommunity,
|
||||
selectedSpace: selectedSpace,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,7 @@ class SpaceManagementTemplatesView extends StatelessWidget {
|
||||
const SpaceManagementTemplatesView({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: ColoredBox(
|
||||
return ColoredBox(
|
||||
color: ColorsManager.whiteColors,
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
|
||||
@ -27,7 +26,6 @@ class SpaceManagementTemplatesView extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent {
|
||||
}
|
||||
|
||||
final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent {
|
||||
final SpaceModel space;
|
||||
final SpaceModel? space;
|
||||
final CommunityModel community;
|
||||
|
||||
const SelectSpaceEvent({required this.space, required this.community});
|
||||
|
@ -0,0 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart';
|
||||
|
||||
abstract final class SpaceDetailsDialogHelper {
|
||||
static void showCreate(BuildContext context) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => const SpaceDetailsDialog(),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SpaceDetailsDialog extends StatelessWidget {
|
||||
const SpaceDetailsDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Dialog(
|
||||
child: Text('Create Space'),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user