refactor: Gott-Datei rooms_panel.dart in 8 Dateien aufgeteilt
Gleiche Technik wie bei settings_modal.dart/message_group.dart: reine Verschiebung per Dart part/part-of, keine Logikänderung. rooms_panel.dart schrumpft von 2555 auf 31 Zeilen (nur noch Bibliothekskopf). Verifiziert per automatisiertem Blockvergleich gegen das Original und flutter analyze. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,608 @@
|
||||
part of '../rooms_panel.dart';
|
||||
|
||||
class _DraggableRoomItem extends StatefulWidget {
|
||||
final Room room;
|
||||
final int index;
|
||||
final bool isActive;
|
||||
final bool isInVoice;
|
||||
final PyramidTheme pt;
|
||||
final VoidCallback onTap;
|
||||
const _DraggableRoomItem({
|
||||
super.key,
|
||||
required this.room,
|
||||
required this.index,
|
||||
required this.isActive,
|
||||
this.isInVoice = false,
|
||||
required this.pt,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_DraggableRoomItem> createState() => _DraggableRoomItemState();
|
||||
}
|
||||
|
||||
class _DraggableRoomItemState extends State<_DraggableRoomItem> {
|
||||
bool _hovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _hovered = true),
|
||||
onExit: (_) => setState(() => _hovered = false),
|
||||
child: Row(children: [
|
||||
ReorderableDragStartListener(
|
||||
index: widget.index,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.grab,
|
||||
child: AnimatedOpacity(
|
||||
opacity: _hovered ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 120),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3),
|
||||
child: Icon(Icons.drag_indicator_rounded,
|
||||
size: 12, color: widget.pt.fgDim),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _RoomItem(
|
||||
room: widget.room,
|
||||
isActive: widget.isActive,
|
||||
isInVoice: widget.isInVoice,
|
||||
pt: widget.pt,
|
||||
onTap: widget.onTap,
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomsList extends ConsumerWidget {
|
||||
final List<Room> rooms;
|
||||
final String filter;
|
||||
final String? spaceId;
|
||||
final Map<String, bool> collapsed;
|
||||
final PyramidTheme pt;
|
||||
final ValueChanged<String> onToggleSection;
|
||||
|
||||
const _RoomsList({
|
||||
required this.rooms,
|
||||
required this.filter,
|
||||
required this.spaceId,
|
||||
required this.collapsed,
|
||||
required this.pt,
|
||||
required this.onToggleSection,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final activeRoomId = ref.watch(activeRoomIdProvider);
|
||||
final isDms = spaceId == 'dms';
|
||||
|
||||
final invites = rooms
|
||||
.where((r) =>
|
||||
r.membership == Membership.invite &&
|
||||
isDms == r.isDirectChat)
|
||||
.toList();
|
||||
|
||||
final filtered = rooms.where((r) {
|
||||
if (r.membership == Membership.invite) return false;
|
||||
if (isDms != r.isDirectChat) return false;
|
||||
if (filter.isEmpty) return true;
|
||||
return r.getLocalizedDisplayname()
|
||||
.toLowerCase()
|
||||
.contains(filter.toLowerCase());
|
||||
}).toList();
|
||||
|
||||
if (filtered.isEmpty && invites.isEmpty) {
|
||||
return Center(
|
||||
child:
|
||||
Text('No rooms', style: TextStyle(color: pt.fgDim, fontSize: 13)));
|
||||
}
|
||||
|
||||
final sectionTitle = isDms ? 'Direct Messages' : 'Rooms';
|
||||
final sectionId = isDms ? 'dms' : 'rooms';
|
||||
final isCollapsed = collapsed[sectionId] ?? false;
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
|
||||
children: [
|
||||
if (invites.isNotEmpty) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 6, 8, 2),
|
||||
child: Text('EINLADUNGEN',
|
||||
style: TextStyle(
|
||||
color: pt.accent,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.6)),
|
||||
),
|
||||
...invites.map((r) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: _InviteItem(room: r, pt: pt))),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
_SectionHeader(
|
||||
title: sectionTitle,
|
||||
count: filtered.length,
|
||||
isCollapsed: isCollapsed,
|
||||
pt: pt,
|
||||
onToggle: () => onToggleSection(sectionId),
|
||||
onAddTap: isDms
|
||||
? () => showDialog(
|
||||
context: context, builder: (_) => const NewDmDialog())
|
||||
: () => showDialog(
|
||||
context: context, builder: (_) => const CreateJoinDialog()),
|
||||
),
|
||||
if (!isCollapsed)
|
||||
...filtered.map((room) => _RoomItem(
|
||||
room: room,
|
||||
isActive: activeRoomId == room.id,
|
||||
pt: pt,
|
||||
onTap: () {
|
||||
ref.read(activeRoomIdProvider.notifier).state = room.id;
|
||||
ref.read(viewModeProvider.notifier).state = ViewMode.chat;
|
||||
},
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends StatefulWidget {
|
||||
final String title;
|
||||
final int count;
|
||||
final bool isCollapsed;
|
||||
final PyramidTheme pt;
|
||||
final VoidCallback onToggle;
|
||||
final VoidCallback? onAddTap;
|
||||
|
||||
const _SectionHeader({
|
||||
required this.title,
|
||||
required this.count,
|
||||
required this.isCollapsed,
|
||||
required this.pt,
|
||||
required this.onToggle,
|
||||
this.onAddTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_SectionHeader> createState() => _SectionHeaderState();
|
||||
}
|
||||
|
||||
class _SectionHeaderState extends State<_SectionHeader> {
|
||||
bool _hovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pt = widget.pt;
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _hovered = true),
|
||||
onExit: (_) => setState(() => _hovered = false),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(8, 6, 8, 4),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: widget.onToggle,
|
||||
child: Row(children: [
|
||||
AnimatedRotation(
|
||||
turns: widget.isCollapsed ? -0.25 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOutBack,
|
||||
child: Icon(Icons.keyboard_arrow_down_rounded,
|
||||
size: 14,
|
||||
color: _hovered ? pt.fgMuted : pt.fgDim),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(widget.title.toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: _hovered ? pt.fgMuted : pt.fgDim,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.04 * 11)),
|
||||
const Spacer(),
|
||||
Text('${widget.count}',
|
||||
style: TextStyle(color: pt.fgDim, fontSize: 11)),
|
||||
]),
|
||||
),
|
||||
),
|
||||
if (_hovered && widget.onAddTap != null) ...[
|
||||
const SizedBox(width: 4),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: widget.onAddTap,
|
||||
child: SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: Center(
|
||||
child: Icon(Icons.add, size: 12, color: pt.fgMuted)),
|
||||
),
|
||||
),
|
||||
] else
|
||||
const SizedBox(width: 22),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomItem extends ConsumerStatefulWidget {
|
||||
final Room room;
|
||||
final bool isActive;
|
||||
final bool isInVoice;
|
||||
final PyramidTheme pt;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _RoomItem({
|
||||
super.key,
|
||||
required this.room,
|
||||
required this.isActive,
|
||||
this.isInVoice = false,
|
||||
required this.pt,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<_RoomItem> createState() => _RoomItemState();
|
||||
}
|
||||
|
||||
class _RoomItemState extends ConsumerState<_RoomItem> {
|
||||
bool _hovered = false;
|
||||
|
||||
Future<void> _leaveRoom() async {
|
||||
final pt = widget.pt;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: pt.bg2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(pt.rXl),
|
||||
side: BorderSide(color: pt.border)),
|
||||
title: Text('Raum verlassen?',
|
||||
style: TextStyle(color: pt.fg, fontSize: 16, fontWeight: FontWeight.w700)),
|
||||
content: Text(
|
||||
'"${widget.room.getLocalizedDisplayname()}" verlassen?',
|
||||
style: TextStyle(color: pt.fgMuted, fontSize: 13)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text('Abbrechen', style: TextStyle(color: pt.fgMuted))),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: pt.danger,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8))),
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('Verlassen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !mounted) return;
|
||||
try {
|
||||
final id = widget.room.id;
|
||||
await widget.room.leave();
|
||||
if (mounted && ref.read(activeRoomIdProvider) == id) {
|
||||
ref.read(activeRoomIdProvider.notifier).state = null;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> _leaveDm() async {
|
||||
final pt = widget.pt;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: pt.bg2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(pt.rXl),
|
||||
side: BorderSide(color: pt.border)),
|
||||
title: Text('Chat verlassen?',
|
||||
style: TextStyle(color: pt.fg, fontSize: 16, fontWeight: FontWeight.w700)),
|
||||
content: Text(
|
||||
'Unterhaltung mit "${widget.room.getLocalizedDisplayname()}" schließen?',
|
||||
style: TextStyle(color: pt.fgMuted, fontSize: 13)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text('Abbrechen', style: TextStyle(color: pt.fgMuted))),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: pt.danger,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8))),
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('Verlassen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !mounted) return;
|
||||
try {
|
||||
final id = widget.room.id;
|
||||
await widget.room.leave();
|
||||
if (mounted) {
|
||||
if (ref.read(activeRoomIdProvider) == id) {
|
||||
ref.read(activeRoomIdProvider.notifier).state = null;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> _showRoomSettings() async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (_) => RoomSettingsDialog(room: widget.room),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _inviteUser() async {
|
||||
final pt = widget.pt;
|
||||
final ctrl = TextEditingController();
|
||||
final userId = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: pt.bg2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(pt.rXl),
|
||||
side: BorderSide(color: pt.border)),
|
||||
title: Text('Nutzer einladen',
|
||||
style: TextStyle(
|
||||
color: pt.fg, fontSize: 16, fontWeight: FontWeight.w700)),
|
||||
content: TextField(
|
||||
controller: ctrl,
|
||||
autofocus: true,
|
||||
style: TextStyle(color: pt.fg, fontSize: 13),
|
||||
decoration: InputDecoration(
|
||||
hintText: '@nutzer:server',
|
||||
hintStyle: TextStyle(color: pt.fgDim, fontSize: 12),
|
||||
filled: true,
|
||||
fillColor: pt.bg3,
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: pt.border)),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: pt.border)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: pt.accent)),
|
||||
),
|
||||
cursorColor: pt.accent,
|
||||
onSubmitted: (v) => Navigator.pop(ctx, v.trim()),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: Text('Abbrechen', style: TextStyle(color: pt.fgMuted))),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, ctrl.text.trim()),
|
||||
child: Text('Einladen', style: TextStyle(color: pt.accent))),
|
||||
],
|
||||
),
|
||||
);
|
||||
ctrl.dispose();
|
||||
if (userId == null || userId.isEmpty || !mounted) return;
|
||||
try {
|
||||
await widget.room.invite(userId);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('Fehler: ${e.toString().split('\n').first}'),
|
||||
backgroundColor: widget.pt.danger,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Touch-friendly action menu — opened via long-press (the hover menu is not
|
||||
/// reachable on touch devices).
|
||||
void _showContextMenu() {
|
||||
final pt = widget.pt;
|
||||
final room = widget.room;
|
||||
final isDm = room.isDirectChat;
|
||||
|
||||
Widget action(IconData icon, String label, VoidCallback onTap,
|
||||
{bool danger = false}) {
|
||||
return ListTile(
|
||||
leading: Icon(icon, size: 20, color: danger ? pt.danger : pt.fgMuted),
|
||||
title: Text(label,
|
||||
style: TextStyle(
|
||||
color: danger ? pt.danger : pt.fg, fontSize: 14)),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
onTap();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: pt.bg2,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
builder: (ctx) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: Text(room.getLocalizedDisplayname(),
|
||||
style: TextStyle(
|
||||
color: pt.fg,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700),
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
]),
|
||||
),
|
||||
Divider(height: 1, color: pt.border),
|
||||
if (!isDm) ...[
|
||||
action(Icons.person_add_outlined, 'Nutzer einladen', _inviteUser),
|
||||
action(Icons.settings_rounded, 'Einstellungen', _showRoomSettings),
|
||||
],
|
||||
action(
|
||||
Icons.exit_to_app_rounded,
|
||||
isDm ? 'Chat verlassen' : 'Raum verlassen',
|
||||
isDm ? _leaveDm : _leaveRoom,
|
||||
danger: true,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pt = widget.pt;
|
||||
final room = widget.room;
|
||||
final active = widget.isActive;
|
||||
final inVoice = widget.isInVoice;
|
||||
final name = room.getLocalizedDisplayname();
|
||||
final unread = room.notificationCount;
|
||||
final isDm = room.isDirectChat;
|
||||
final isVoice = _isVoiceRoom(room);
|
||||
final voiceParticipants = isVoice ? ref.watch(voiceParticipantsProvider(room.id)) : <Map<String, dynamic>>[];
|
||||
final client = ref.watch(matrixClientProvider).valueOrNull;
|
||||
|
||||
Color textColor = (active || inVoice || unread > 0 || _hovered) ? pt.fg : pt.fgMuted;
|
||||
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (_) => setState(() => _hovered = true),
|
||||
onExit: (_) => setState(() => _hovered = false),
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
onLongPress: _showContextMenu,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (active)
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 6,
|
||||
bottom: 6,
|
||||
child: Container(
|
||||
width: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: pt.accent,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(2),
|
||||
bottomRight: Radius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.easeOutCubic,
|
||||
margin: const EdgeInsets.symmetric(vertical: 1),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: pt.rowPadX, vertical: pt.rowPadY),
|
||||
decoration: BoxDecoration(
|
||||
color: active
|
||||
? pt.accentSoft
|
||||
: inVoice
|
||||
? const Color(0xFF57F287).withAlpha(25)
|
||||
: _hovered ? pt.bgHover : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(pt.rSm),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (isDm) ...[
|
||||
_DmAvatar(room: room, pt: pt),
|
||||
] else ...[
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: Icon(
|
||||
isVoice ? Icons.mic_rounded : _roomIcon(room),
|
||||
size: 14,
|
||||
color: isVoice
|
||||
? (inVoice ? const Color(0xFF57F287) : _hovered ? const Color(0xFF57F287) : const Color(0xFF57F287).withAlpha(180))
|
||||
: (active ? pt.accent : _hovered ? pt.fg : pt.fgDim),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 14,
|
||||
fontWeight: unread > 0 || active || inVoice
|
||||
? FontWeight.w600
|
||||
: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (isVoice && voiceParticipants.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Text(
|
||||
'${voiceParticipants.length}',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF57F287).withAlpha(200),
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
if (!isVoice && _hovered) ...[
|
||||
if (isDm)
|
||||
_HoverAction(icon: Icons.close, pt: pt, onTap: _leaveDm)
|
||||
else
|
||||
_RoomMenuBtn(
|
||||
pt: pt,
|
||||
onInvite: _inviteUser,
|
||||
onSettings: _showRoomSettings,
|
||||
onLeave: _leaveRoom,
|
||||
),
|
||||
] else if (!isVoice && unread > 0) ...[
|
||||
_UnreadBadge(
|
||||
count: unread,
|
||||
isMention: room.highlightCount > 0,
|
||||
pt: pt),
|
||||
],
|
||||
],
|
||||
),
|
||||
// Voice participants (Discord-style named rows)
|
||||
if (isVoice && voiceParticipants.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2, left: 24),
|
||||
child: _VoiceParticipantList(
|
||||
participants: voiceParticipants,
|
||||
client: client,
|
||||
pt: pt,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _roomIcon(Room room) {
|
||||
if (room.encrypted) return Icons.lock_outline_rounded;
|
||||
return Icons.tag_rounded;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user