Отображение чатов

This commit is contained in:
Халимов Рустам
2026-05-14 22:56:42 +03:00
parent ccffc5a53c
commit 0b013def2e
29 changed files with 6365 additions and 842 deletions
@@ -1,13 +1,21 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:messenger_app/l10n/app_localizations.dart';
import '../bloc/chat_bloc.dart';
import '../bloc/chat_event.dart';
import '../bloc/chat_state.dart';
import '../../domain/entities/chat.dart';
import '../../domain/entities/message.dart';
class ChatDetailPage extends StatefulWidget {
final String chatId;
final String chatTitle;
final Chat chat;
final String currentUserId;
const ChatDetailPage({
super.key,
required this.chatId,
required this.chatTitle,
required this.chat,
required this.currentUserId,
});
@override
@@ -16,90 +24,148 @@ class ChatDetailPage extends StatefulWidget {
class _ChatDetailPageState extends State<ChatDetailPage> {
final _messageController = TextEditingController();
final _scrollController = ScrollController();
@override
void dispose() {
_messageController.dispose();
_scrollController.dispose();
super.dispose();
}
void _sendMessage() {
if (_messageController.text.trim().isEmpty) return;
_messageController.clear();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.chatTitle),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.chat.type == 'favorites' ? AppLocalizations.of(context)!.favorites : widget.chat.title),
if (widget.chat.type != 'favorites')
Text(
AppLocalizations.of(context)!.connected,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
),
],
),
actions: [
IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}),
],
),
body: const Center(child: Text('Chat Detail')),
bottomNavigationBar: _MessageInput(
controller: _messageController,
onSend: _sendMessage,
body: Column(
children: [
Expanded(
child: BlocBuilder<ChatBloc, ChatState>(
builder: (context, state) {
return state.maybeWhen(
chatSelected: (_, messages) => _buildMessageList(messages),
messagesLoaded: (messages) => _buildMessageList(messages),
loading: () => const Center(child: CircularProgressIndicator()),
error: (msg) => Center(child: Text('Ошибка: $msg')),
orElse: () => const Center(child: Text('Начните общение')),
);
},
),
),
_buildMessageInput(),
],
),
);
}
}
class _MessageInput extends StatelessWidget {
final TextEditingController controller;
final VoidCallback onSend;
Widget _buildMessageList(List<Message> messages) {
if (messages.isEmpty) {
return const Center(child: Text('Сообщений пока нет'));
}
const _MessageInput({
required this.controller,
required this.onSend,
});
return ListView.builder(
controller: _scrollController,
reverse: true,
padding: const EdgeInsets.all(16),
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[index];
final isMe = message.senderId == widget.currentUserId;
return Align(
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: isMe ? Theme.of(context).primaryColor : Colors.grey[200],
borderRadius: BorderRadius.circular(16).copyWith(
bottomRight: isMe ? const Radius.circular(0) : const Radius.circular(16),
bottomLeft: !isMe ? const Radius.circular(0) : const Radius.circular(16),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
message.content,
style: TextStyle(color: isMe ? Colors.white : Colors.black87),
),
const SizedBox(height: 4),
if (message.createdAt != null)
Text(
DateFormat.Hm().format(message.createdAt!),
style: TextStyle(
fontSize: 10,
color: isMe ? Colors.white70 : Colors.black45,
),
),
],
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
Widget _buildMessageInput() {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
color: Colors.black.withOpacity(0.05),
offset: const Offset(0, -1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.attach_file),
onPressed: () {},
),
Expanded(
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: 'Сообщение',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey[200],
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
child: SafeArea(
child: Row(
children: [
IconButton(icon: const Icon(Icons.add), onPressed: () {}),
Expanded(
child: TextField(
controller: _messageController,
decoration: const InputDecoration(
hintText: 'Сообщение',
border: InputBorder.none,
),
onSubmitted: (_) => _sendMessage(),
),
maxLines: null,
onSubmitted: (_) => onSend(),
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.send),
color: Theme.of(context).primaryColor,
onPressed: onSend,
),
],
IconButton(
icon: const Icon(Icons.send),
onPressed: _sendMessage,
color: Theme.of(context).primaryColor,
),
],
),
),
);
}
void _sendMessage() {
final content = _messageController.text.trim();
if (content.isNotEmpty) {
context.read<ChatBloc>().add(ChatEvent.messageSent(widget.chat.id, content));
_messageController.clear();
}
}
}
@@ -1,35 +1,169 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/chat_bloc.dart';
import '../bloc/chat_state.dart';
import 'package:intl/intl.dart';
import 'package:messenger_app/features/chat/presentation/bloc/chat_bloc.dart';
import 'package:messenger_app/features/chat/presentation/bloc/chat_event.dart';
import 'package:messenger_app/features/chat/presentation/bloc/chat_state.dart';
import 'package:messenger_app/features/auth/presentation/bloc/auth_bloc.dart';
import 'package:messenger_app/features/auth/presentation/bloc/auth_state.dart';
import 'package:messenger_app/features/chat/domain/entities/chat.dart';
import 'package:messenger_app/l10n/app_localizations.dart';
import 'chat_detail_page.dart';
class ChatsPage extends StatelessWidget {
class ChatsPage extends StatefulWidget {
const ChatsPage({super.key});
@override
State<ChatsPage> createState() => _ChatsPageState();
}
class _ChatsPageState extends State<ChatsPage> {
@override
void initState() {
super.initState();
final authState = context.read<AuthBloc>().state;
final userId = authState.maybeWhen(
authenticated: (id) => id,
orElse: () => null,
);
context.read<ChatBloc>().add(ChatEvent.chatsLoaded(currentUserId: userId));
}
@override
Widget build(BuildContext context) {
return BlocBuilder<ChatBloc, ChatState>(
builder: (context, state) {
return state.when(
initial: () => const Center(child: CircularProgressIndicator()),
loading: () => const Center(child: CircularProgressIndicator()),
chatsLoaded: (_) => const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.chat_bubble_outline, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('У вас пока нет чатов'),
],
),
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.chats),
actions: [
IconButton(
icon: const Icon(Icons.bookmark_border),
onPressed: () {
context.read<ChatBloc>().add(const ChatEvent.favoritesRequested());
},
),
chatSelected: (_, __) => const Center(child: Text('Chat selected')),
messagesLoaded: (_) => const Center(child: Text('Messages loaded')),
messageSent: () => const Center(child: Text('Message sent')),
error: (message) => Center(child: Text('Ошибка: $message')),
],
),
body: BlocConsumer<ChatBloc, ChatState>(
listener: (context, state) {
state.whenOrNull(
chatSelected: (chat, _) {
final authState = context.read<AuthBloc>().state;
final userId = authState.maybeWhen(
authenticated: (id) => id,
orElse: () => '',
);
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ChatDetailPage(
chat: chat,
currentUserId: userId,
),
),
).then((_) {
// Reload chats when returning from detail page
if (context.mounted) {
context.read<ChatBloc>().add(ChatEvent.chatsLoaded(currentUserId: userId));
}
});
},
);
},
builder: (context, state) {
return state.maybeWhen(
loading: () => const Center(child: CircularProgressIndicator()),
chatsLoaded: (chats) => _buildChatList(chats, l10n),
error: (message) => Center(child: Text('${l10n.error}: $message')),
orElse: () => const Center(child: CircularProgressIndicator()),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// TODO: Open contact list to create chat
},
child: const Icon(Icons.edit),
),
);
}
Widget _buildChatList(List<Chat> chats, AppLocalizations l10n) {
if (chats.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.chat_bubble_outline, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text(l10n.noChats),
const SizedBox(height: 8),
TextButton(
onPressed: () {
final authState = context.read<AuthBloc>().state;
final userId = authState.maybeWhen(
authenticated: (id) => id,
orElse: () => null,
);
context.read<ChatBloc>().add(ChatEvent.chatsLoaded(currentUserId: userId));
},
child: Text(l10n.retry),
),
],
),
);
}
return ListView.separated(
itemCount: chats.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final chat = chats[index];
return ListTile(
leading: CircleAvatar(
backgroundImage: chat.avatarUrl != null ? NetworkImage(chat.avatarUrl!) : null,
child: chat.avatarUrl == null
? Text(chat.title.isNotEmpty ? chat.title[0].toUpperCase() : '?')
: null,
),
title: Text(
chat.type == 'favorites' ? l10n.favorites : chat.title,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
chat.lastMessage?.content ?? l10n.noChats,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (chat.lastMessageTime != null)
Text(
DateFormat.Hm().format(chat.lastMessageTime!),
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 4),
if (chat.unreadCount > 0)
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(10),
),
child: Text(
chat.unreadCount.toString(),
style: const TextStyle(color: Colors.white, fontSize: 10),
),
),
],
),
onTap: () {
context.read<ChatBloc>().add(ChatEvent.chatSelected(chat.id));
},
);
},
);
}
}