refactor: Projektstruktur in app/ und backend/ aufgeteilt
This commit is contained in:
692
app/lib/chat_details_view.dart
Normal file
692
app/lib/chat_details_view.dart
Normal file
@@ -0,0 +1,692 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'app_state.dart';
|
||||
import 'models/chat.dart';
|
||||
import 'models/chat_message.dart';
|
||||
import 'services/chat_service.dart';
|
||||
import 'services/websocket_service.dart';
|
||||
import 'services/notification_service.dart';
|
||||
import 'widgets/chat_photo_dialog.dart';
|
||||
import 'widgets/offline_banner.dart';
|
||||
|
||||
class ChatDetailsView extends StatefulWidget {
|
||||
final Chat chat;
|
||||
|
||||
const ChatDetailsView({super.key, required this.chat});
|
||||
|
||||
@override
|
||||
State<ChatDetailsView> createState() => _ChatDetailsViewState();
|
||||
}
|
||||
|
||||
class _PreparedImage {
|
||||
const _PreparedImage({required this.base64DataUri, required this.bytes});
|
||||
|
||||
final String base64DataUri;
|
||||
final Uint8List bytes;
|
||||
}
|
||||
|
||||
class _ChatDetailsViewState extends State<ChatDetailsView> {
|
||||
final TextEditingController _messageController = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
late List<ChatMessage> _messages;
|
||||
final WebSocketService _webSocketService = WebSocketService();
|
||||
StreamSubscription<List<Chat>>? _chatsStreamSubscription;
|
||||
String? _currentUserId;
|
||||
late final String _conversationKey;
|
||||
final ChatService _chatService = ChatService();
|
||||
final Map<String, Uint8List> _imageCache = <String, Uint8List>{};
|
||||
late Chat _activeChat;
|
||||
static const int _maxDisplayMessages = 30;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_activeChat = widget.chat;
|
||||
_conversationKey = _activeChat.id;
|
||||
NotificationService().activeConversationKey = _conversationKey;
|
||||
_messages = _lastMessages(_activeChat.messages);
|
||||
_currentUserId = AppState().loggedInEmail;
|
||||
|
||||
_chatsStreamSubscription = _chatService.chatsStream.listen(
|
||||
_handleChatsUpdate,
|
||||
);
|
||||
|
||||
_chatService.initialize().then((_) async {
|
||||
_syncActiveChatFromService(replaceMessages: _messages.isEmpty);
|
||||
final history = await _chatService.loadMessagesForChat(_conversationKey);
|
||||
if (!mounted) return;
|
||||
if (history.isNotEmpty) {
|
||||
setState(() {
|
||||
_imageCache.clear();
|
||||
_messages = _lastMessages(history);
|
||||
});
|
||||
_scrollToBottom(immediate: true);
|
||||
}
|
||||
_syncActiveChatFromService();
|
||||
await _chatService.markConversationRead(_conversationKey);
|
||||
});
|
||||
|
||||
// Scroll to bottom after initial build is complete
|
||||
_scrollToBottom(immediate: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (NotificationService().activeConversationKey == _conversationKey) {
|
||||
NotificationService().activeConversationKey = null;
|
||||
}
|
||||
_chatsStreamSubscription?.cancel();
|
||||
_messageController.dispose();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _scrollToBottom({bool immediate = false}) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!_scrollController.hasClients) {
|
||||
return;
|
||||
}
|
||||
final target = _scrollController.position.maxScrollExtent;
|
||||
if (immediate) {
|
||||
_scrollController.jumpTo(target);
|
||||
} else {
|
||||
_scrollController.animateTo(
|
||||
target,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _handleChatsUpdate(List<Chat> chats) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final updated = _findChatById(chats);
|
||||
if (updated == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final shouldReplace = _shouldReplaceMessages(updated);
|
||||
|
||||
setState(() {
|
||||
_activeChat = updated;
|
||||
if (shouldReplace) {
|
||||
_imageCache.clear();
|
||||
_messages = _lastMessages(updated.messages);
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldReplace) {
|
||||
_scrollToBottom();
|
||||
unawaited(_chatService.markConversationRead(_conversationKey));
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldReplaceMessages(Chat chat) {
|
||||
if (chat.messages.length != _messages.length) {
|
||||
return true;
|
||||
}
|
||||
if (_messages.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final currentLast = _messages.last;
|
||||
final updatedLast = chat.messages.last;
|
||||
return currentLast.id != updatedLast.id ||
|
||||
currentLast.content != updatedLast.content ||
|
||||
currentLast.contentType != updatedLast.contentType;
|
||||
}
|
||||
|
||||
List<ChatMessage> _lastMessages(List<ChatMessage> messages) {
|
||||
final sorted = List<ChatMessage>.from(messages)
|
||||
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||
if (sorted.length > _maxDisplayMessages) {
|
||||
return sorted.sublist(sorted.length - _maxDisplayMessages);
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
Chat? _findChatById(List<Chat> chats) {
|
||||
for (final chat in chats) {
|
||||
if (chat.id == _conversationKey) {
|
||||
return chat;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _syncActiveChatFromService({bool replaceMessages = false}) {
|
||||
final updated = _findChatById(_chatService.currentChats);
|
||||
if (updated == null || !mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final shouldReplace = replaceMessages || _shouldReplaceMessages(updated);
|
||||
|
||||
setState(() {
|
||||
_activeChat = updated;
|
||||
if (shouldReplace) {
|
||||
_imageCache.clear();
|
||||
_messages = _lastMessages(updated.messages);
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldReplace) {
|
||||
_scrollToBottom();
|
||||
unawaited(_chatService.markConversationRead(_conversationKey));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendMessage() async {
|
||||
final text = _messageController.text.trim();
|
||||
if (text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final sender = _currentUserId;
|
||||
final receiver = _activeChat.receiver;
|
||||
|
||||
if (sender == null || sender.isEmpty) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).noSenderMessage),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (receiver == null || receiver.isEmpty) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).noRecipientMessage),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await _webSocketService.sendChatMessage(
|
||||
sender: sender,
|
||||
receiver: receiver,
|
||||
content: text,
|
||||
jobId: _activeChat.jobId,
|
||||
jobNumber: _activeChat.jobNumber,
|
||||
);
|
||||
|
||||
if (result == null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).messageSendError),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await _chatService.saveOutgoingMessage(result);
|
||||
_syncActiveChatFromService();
|
||||
|
||||
_messageController.clear();
|
||||
|
||||
_scrollToBottom();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isJobChat = _activeChat.type == ChatType.jobSpecific;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(_activeChat.title, style: const TextStyle(fontSize: 16)),
|
||||
if (isJobChat && _activeChat.jobNumber != null)
|
||||
Text(
|
||||
'Job-Nr: ${_activeChat.jobNumber}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.deepPurple[100],
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(isJobChat ? Icons.work : Icons.support_agent),
|
||||
onPressed: () {
|
||||
// Show chat info
|
||||
_showChatInfo();
|
||||
},
|
||||
tooltip: AppLocalizations.of(context).chatInfo,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
const OfflineBanner(),
|
||||
// Messages list
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: Colors.grey[50]),
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 96),
|
||||
itemCount: _messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
final message = _messages[index];
|
||||
return _buildMessageBubble(message);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
// Message input
|
||||
_buildMessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageBubble(ChatMessage message) {
|
||||
final isOwn = message.isOwn;
|
||||
final isImage = message.contentType == ChatContentType.image;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
isOwn ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: [
|
||||
if (!isOwn) const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.7,
|
||||
),
|
||||
margin: EdgeInsets.only(
|
||||
left: isOwn ? 40 : 0,
|
||||
right: isOwn ? 0 : 40,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isImage ? 6 : 12,
|
||||
vertical: isImage ? 6 : 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isOwn ? Colors.deepPurple[100] : Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: const Radius.circular(12),
|
||||
topRight: const Radius.circular(12),
|
||||
bottomLeft: Radius.circular(isOwn ? 12 : 4),
|
||||
bottomRight: Radius.circular(isOwn ? 4 : 12),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildMessageContent(message, isImage: isImage),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
_formatMessageTime(message.createdAt),
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
|
||||
),
|
||||
if (isOwn) ...[
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
message.pendingSync
|
||||
? Icons.schedule
|
||||
: (message.read ? Icons.done_all : Icons.done),
|
||||
size: 14,
|
||||
color:
|
||||
message.pendingSync
|
||||
? Colors.orange[700]
|
||||
: (message.read
|
||||
? Colors.deepPurple[400]
|
||||
: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isOwn) const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageContent(ChatMessage message, {required bool isImage}) {
|
||||
if (!isImage) {
|
||||
return Text(
|
||||
message.content,
|
||||
style: TextStyle(fontSize: 15, color: Colors.grey[800]),
|
||||
);
|
||||
}
|
||||
|
||||
final imageBytes = _imageCache[message.id] ?? _decodeImageBytes(message);
|
||||
|
||||
if (imageBytes == null) {
|
||||
return const Text(
|
||||
'Bild konnte nicht geladen werden.',
|
||||
style: TextStyle(fontSize: 15),
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _showImagePreview(imageBytes),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
constraints: const BoxConstraints(maxWidth: 260, minWidth: 140),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 4 / 3,
|
||||
child: Image.memory(imageBytes, fit: BoxFit.cover),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List? _decodeImageBytes(ChatMessage message) {
|
||||
final rawContent = message.content.trim();
|
||||
if (rawContent.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final base64Payload =
|
||||
rawContent.startsWith('data:')
|
||||
? rawContent.substring(rawContent.indexOf(',') + 1)
|
||||
: rawContent;
|
||||
|
||||
final normalized = base64Payload.replaceAll(RegExp(r'\s'), '');
|
||||
|
||||
try {
|
||||
final bytes = base64Decode(normalized);
|
||||
_imageCache[message.id] = bytes;
|
||||
return bytes;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showImagePreview(Uint8List imageBytes) async {
|
||||
if (!mounted) return;
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.all(16),
|
||||
backgroundColor: Colors.black,
|
||||
child: InteractiveViewer(
|
||||
child: Image.memory(imageBytes, fit: BoxFit.contain),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageInput() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.grey[300]!)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: _handleAttachmentTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.attach_file,
|
||||
color: Colors.black87,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _messageController,
|
||||
decoration: InputDecoration(
|
||||
hintText: AppLocalizations.of(context).typeMessage,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
_sendMessage();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.deepPurple,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(Icons.send, color: Colors.white, size: 20),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleAttachmentTap() async {
|
||||
if (!mounted) return;
|
||||
final Uint8List? photoBytes = await showDialog<Uint8List>(
|
||||
context: context,
|
||||
builder: (context) => const ChatPhotoDialog(),
|
||||
);
|
||||
|
||||
if (photoBytes == null || photoBytes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _sendImageMessage(photoBytes);
|
||||
}
|
||||
|
||||
Future<void> _sendImageMessage(Uint8List imageBytes) async {
|
||||
final sender = _currentUserId;
|
||||
final receiver = _activeChat.receiver;
|
||||
|
||||
if (sender == null || sender.isEmpty) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).noSenderMessage),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (receiver == null || receiver.isEmpty) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).noRecipientMessage),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final prepared = await _prepareImagePayload(imageBytes);
|
||||
if (prepared == null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).photoProcessError),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await _webSocketService.sendChatMessage(
|
||||
sender: sender,
|
||||
receiver: receiver,
|
||||
content: prepared.base64DataUri,
|
||||
contentType: ChatContentType.image,
|
||||
jobId: _activeChat.jobId,
|
||||
jobNumber: _activeChat.jobNumber,
|
||||
);
|
||||
|
||||
if (result == null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(AppLocalizations.of(context).imageSendError)),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await _chatService.saveOutgoingMessage(result);
|
||||
_syncActiveChatFromService();
|
||||
|
||||
if (prepared.bytes.isNotEmpty) {
|
||||
_imageCache[result.id] = prepared.bytes;
|
||||
}
|
||||
}
|
||||
|
||||
Future<_PreparedImage?> _prepareImagePayload(Uint8List originalBytes) async {
|
||||
try {
|
||||
final decoded = img.decodeImage(originalBytes);
|
||||
if (decoded == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final baked = img.bakeOrientation(decoded);
|
||||
const maxDimension = 1280;
|
||||
img.Image processed = baked;
|
||||
|
||||
if (baked.width > maxDimension || baked.height > maxDimension) {
|
||||
final scale =
|
||||
baked.width > baked.height
|
||||
? maxDimension / baked.width
|
||||
: maxDimension / baked.height;
|
||||
final targetWidth = (baked.width * scale).round();
|
||||
final targetHeight = (baked.height * scale).round();
|
||||
processed = img.copyResize(
|
||||
baked,
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
interpolation: img.Interpolation.average,
|
||||
);
|
||||
}
|
||||
|
||||
final encodedBytes = Uint8List.fromList(
|
||||
img.encodeJpg(processed, quality: 85),
|
||||
);
|
||||
final base64Payload = base64Encode(encodedBytes);
|
||||
final dataUri = 'data:image/jpeg;base64,$base64Payload';
|
||||
|
||||
return _PreparedImage(base64DataUri: dataUri, bytes: encodedBytes);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatMessageTime(DateTime dateTime) {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final messageDate = DateTime(dateTime.year, dateTime.month, dateTime.day);
|
||||
|
||||
if (messageDate == today) {
|
||||
// Today - show only time
|
||||
return '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
|
||||
} else if (messageDate == today.subtract(const Duration(days: 1))) {
|
||||
// Yesterday
|
||||
return 'Gestern ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
|
||||
} else {
|
||||
// Older - show date and time
|
||||
return '${dateTime.day.toString().padLeft(2, '0')}.${dateTime.month.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
void _showChatInfo() {
|
||||
final isJobChat = _activeChat.type == ChatType.jobSpecific;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(_activeChat.title),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${AppLocalizations.of(context).status}: ${isJobChat ? AppLocalizations.of(context).chatTypeJob : AppLocalizations.of(context).chatTypeGeneral}'),
|
||||
const SizedBox(height: 8),
|
||||
if (isJobChat && _activeChat.jobNumber != null) ...[
|
||||
Text('${AppLocalizations.of(context).jobNumber}: ${_activeChat.jobNumber}'),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
Text('${AppLocalizations.of(context).messages}: ${_messages.length}'),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Erstellt: ${_formatMessageTime(_messages.isNotEmpty ? _messages.first.createdAt : DateTime.now())}',
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).close),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user