refactor: improve specialization card layout and unify chat screen header and navigation with home screen components.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:real_estate_mobile/config/app_config.dart';
|
||||
import 'package:real_estate_mobile/core/constants/app_colors.dart';
|
||||
import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart';
|
||||
@@ -91,7 +92,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
||||
|
||||
void _handleBack() {
|
||||
ref.read(messagingProvider.notifier).setActiveConversation(null);
|
||||
Navigator.pop(context);
|
||||
context.go('/messages');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -183,51 +184,37 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
||||
final otherPartyName = conversation?.otherParty.name ?? 'Chat';
|
||||
final otherPartyAvatar = conversation?.otherParty.avatar;
|
||||
final isOnline = conversation?.otherParty.isOnline ?? false;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildHeader(
|
||||
name: otherPartyName,
|
||||
isOnline: isOnline,
|
||||
),
|
||||
Expanded(
|
||||
child: messagingState.isLoadingMessages && messages.isEmpty
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.accentOrange,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: messages.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'No messages yet.\nSay hello!',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.hintText,
|
||||
),
|
||||
),
|
||||
)
|
||||
: _buildMessagesList(
|
||||
messages: messages,
|
||||
currentUserId: currentUserId ?? '',
|
||||
currentUserName: 'You',
|
||||
otherPartyName: otherPartyName,
|
||||
otherPartyAvatar: otherPartyAvatar,
|
||||
isTyping: isTyping,
|
||||
isLoadingMore: messagingState.isLoadingMessages &&
|
||||
messages.isNotEmpty,
|
||||
),
|
||||
),
|
||||
_buildInputArea(),
|
||||
],
|
||||
final otherPartyHeadline = conversation?.otherParty.headline;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Chat header with name + status
|
||||
_buildHeader(
|
||||
name: otherPartyName,
|
||||
isOnline: isOnline,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: messagingState.isLoadingMessages && messages.isEmpty
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.accentOrange,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: _buildChatBody(
|
||||
messages: messages,
|
||||
currentUserId: currentUserId ?? '',
|
||||
otherPartyName: otherPartyName,
|
||||
otherPartyAvatar: otherPartyAvatar,
|
||||
otherPartyHeadline: otherPartyHeadline,
|
||||
isOnline: isOnline,
|
||||
isTyping: isTyping,
|
||||
isLoadingMore: messagingState.isLoadingMessages &&
|
||||
messages.isNotEmpty,
|
||||
),
|
||||
),
|
||||
_buildInputArea(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -240,104 +227,277 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
||||
required bool isOnline,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Container(
|
||||
height: 55,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: AppColors.primaryDark,
|
||||
width: 0.5,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Back arrow
|
||||
GestureDetector(
|
||||
onTap: _handleBack,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 14),
|
||||
child: Icon(
|
||||
Icons.arrow_back,
|
||||
size: 23,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 6),
|
||||
child: Row(
|
||||
children: [
|
||||
// Back arrow
|
||||
GestureDetector(
|
||||
onTap: _handleBack,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(right: 16),
|
||||
child: Icon(
|
||||
Icons.arrow_back,
|
||||
size: 22,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
// Name and active status
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
// Name and active status
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
if (isOnline) ...[
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF4CAF50),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
],
|
||||
Text(
|
||||
isOnline ? 'Active Now' : 'Offline',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.primaryDark,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
if (isOnline) ...[
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF4CAF50),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
],
|
||||
),
|
||||
],
|
||||
Text(
|
||||
isOnline ? 'Active Now' : 'Offline',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Three dots (vertical)
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Icon(
|
||||
Icons.more_vert,
|
||||
size: 20,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
// Three dots (vertical)
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Icon(
|
||||
Icons.more_vert,
|
||||
size: 20,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
// Star icon
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Icon(
|
||||
Icons.star_rounded,
|
||||
size: 24,
|
||||
color: AppColors.accentOrange,
|
||||
),
|
||||
),
|
||||
// Star icon
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 14),
|
||||
child: Icon(
|
||||
Icons.star_rounded,
|
||||
size: 24,
|
||||
color: AppColors.accentOrange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CHAT BODY (profile section + messages)
|
||||
// ============================================================
|
||||
|
||||
Widget _buildChatBody({
|
||||
required List<ChatMessage> messages,
|
||||
required String currentUserId,
|
||||
required String otherPartyName,
|
||||
String? otherPartyAvatar,
|
||||
String? otherPartyHeadline,
|
||||
required bool isOnline,
|
||||
required bool isTyping,
|
||||
required bool isLoadingMore,
|
||||
}) {
|
||||
if (messages.isEmpty) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildProfileSection(
|
||||
name: otherPartyName,
|
||||
avatar: otherPartyAvatar,
|
||||
headline: otherPartyHeadline,
|
||||
isOnline: isOnline,
|
||||
),
|
||||
const SizedBox(height: 60),
|
||||
Text(
|
||||
'No messages yet.\nSay hello!',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.hintText,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return _buildMessagesList(
|
||||
messages: messages,
|
||||
currentUserId: currentUserId,
|
||||
currentUserName: 'You',
|
||||
otherPartyName: otherPartyName,
|
||||
otherPartyAvatar: otherPartyAvatar,
|
||||
otherPartyHeadline: otherPartyHeadline,
|
||||
isOnline: isOnline,
|
||||
isTyping: isTyping,
|
||||
isLoadingMore: isLoadingMore,
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PROFILE SECTION (large avatar, name, specialties)
|
||||
// ============================================================
|
||||
|
||||
Widget _buildProfileSection({
|
||||
required String name,
|
||||
String? avatar,
|
||||
String? headline,
|
||||
required bool isOnline,
|
||||
}) {
|
||||
final avatarUrl = _resolveAvatarUrl(avatar);
|
||||
final initials = _getInitials(name);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Large avatar with online dot
|
||||
Stack(
|
||||
children: [
|
||||
ClipOval(
|
||||
child: avatarUrl.isNotEmpty
|
||||
? CachedNetworkImage(
|
||||
imageUrl: avatarUrl,
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (_, __, ___) =>
|
||||
_AvatarFallback(letter: initials, size: 80),
|
||||
)
|
||||
: _AvatarFallback(letter: initials, size: 80),
|
||||
),
|
||||
if (isOnline)
|
||||
Positioned(
|
||||
right: 2,
|
||||
bottom: 2,
|
||||
child: Container(
|
||||
width: 14,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF4CAF50),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
// Name with pronouns and orange dot
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'(He/Him)',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.accentOrange,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Specialties row
|
||||
if (headline != null && headline.isNotEmpty)
|
||||
_buildSpecialtiesRow(headline)
|
||||
else
|
||||
_buildSpecialtiesRow('Sales,Analytics,Inspection,Residential,Commercial'),
|
||||
const SizedBox(height: 12),
|
||||
// Divider
|
||||
Divider(
|
||||
color: AppColors.primaryDark.withValues(alpha: 0.1),
|
||||
thickness: 0.5,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSpecialtiesRow(String headline) {
|
||||
// Split by comma, pipe, or similar separators
|
||||
final specialties = headline.split(RegExp(r'[,|;]'))
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
if (specialties.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
for (int i = 0; i < specialties.length; i++) ...[
|
||||
Text(
|
||||
specialties[i],
|
||||
style: const TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
if (i < specialties.length - 1)
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -352,16 +512,19 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
||||
required String currentUserName,
|
||||
required String otherPartyName,
|
||||
String? otherPartyAvatar,
|
||||
String? otherPartyHeadline,
|
||||
required bool isOnline,
|
||||
required bool isTyping,
|
||||
required bool isLoadingMore,
|
||||
}) {
|
||||
// +1 for the profile section header at the end of reversed list
|
||||
final itemCount =
|
||||
messages.length + (isLoadingMore ? 1 : 0) + (isTyping ? 1 : 0);
|
||||
messages.length + 1 + (isLoadingMore ? 1 : 0) + (isTyping ? 1 : 0);
|
||||
|
||||
return ListView.builder(
|
||||
controller: _scrollController,
|
||||
reverse: true,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (isTyping && index == 0) {
|
||||
@@ -370,6 +533,17 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
||||
|
||||
final adjustedIndex = isTyping ? index - 1 : index;
|
||||
|
||||
// Profile section at the top (last index in reversed list)
|
||||
final profileIndex = messages.length + (isLoadingMore ? 1 : 0);
|
||||
if (adjustedIndex == profileIndex) {
|
||||
return _buildProfileSection(
|
||||
name: otherPartyName,
|
||||
avatar: otherPartyAvatar,
|
||||
headline: otherPartyHeadline,
|
||||
isOnline: isOnline,
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoadingMore && adjustedIndex == messages.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
@@ -404,19 +578,22 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
||||
: otherPartyName;
|
||||
final senderAvatar = isMine ? null : otherPartyAvatar;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (showDateSeparator)
|
||||
_buildDateSeparator(
|
||||
DateTime.parse(message.createdAt).toLocal(),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
if (showDateSeparator)
|
||||
_buildDateSeparator(
|
||||
DateTime.parse(message.createdAt).toLocal(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: isMine
|
||||
? _buildSentMessage(message, senderName)
|
||||
: _buildReceivedMessage(message, senderName, senderAvatar),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: isMine
|
||||
? _buildSentMessage(message, senderName)
|
||||
: _buildReceivedMessage(message, senderName, senderAvatar),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -495,6 +672,16 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'He/Him',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
@@ -567,6 +754,16 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'He/Him',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SourceSerif4',
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
timestamp,
|
||||
style: const TextStyle(
|
||||
@@ -667,41 +864,30 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Plus icon in circle border
|
||||
// Plus icon (plain, no border)
|
||||
GestureDetector(
|
||||
onTap: _showAttachmentOptions,
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColors.primaryDark,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
size: 24,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(right: 10),
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
size: 24,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
|
||||
// Text input
|
||||
// Text input pill with send icon inside
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 38,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: AppColors.primaryDark,
|
||||
width: 0.5,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
@@ -736,7 +922,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Send icon inside the input field
|
||||
// Send arrow inside the pill
|
||||
GestureDetector(
|
||||
onTap: _sendMessage,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
@@ -755,25 +941,21 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
|
||||
// Mic icon in circle border
|
||||
// Mic icon in filled dark circle
|
||||
GestureDetector(
|
||||
onTap: () => _showComingSoon('Speech to text'),
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.primaryDark,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColors.primaryDark,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.mic_none,
|
||||
size: 20,
|
||||
color: AppColors.primaryDark,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -873,6 +1055,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// -- Avatar Fallback --
|
||||
|
||||
Reference in New Issue
Block a user