Повторный вход в аккаунт, очистка кэша

This commit is contained in:
Халимов Рустам
2026-05-15 00:10:34 +03:00
parent 0b013def2e
commit 40589dbb75
21 changed files with 1732 additions and 309 deletions
@@ -9,6 +9,7 @@ import 'chat_state.dart';
class ChatBloc extends Bloc<ChatEvent, ChatState> {
final ChatRepository chatRepository;
StreamSubscription? _messageSubscription;
StreamSubscription? _typingSubscription;
ChatBloc({required this.chatRepository}) : super(const ChatState.initial()) {
on<ChatEventStarted>(_onStarted);
@@ -17,20 +18,29 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
on<ChatEventMessagesRequested>(_onMessagesRequested);
on<ChatEventMessageSent>(_onMessageSent);
on<ChatEventFavoritesRequested>(_onFavoritesRequested);
on<ChatEventTypingUpdated>(_onTypingUpdated);
on<ChatEventSendTypingStatus>(_onSendTypingStatus);
on<ChatEventCacheCleared>(_onCacheCleared);
_messageSubscription = chatRepository.messageStream.listen((message) {
// Handle real-time messages
add(ChatEvent.messagesRequested(message.chatId));
});
_typingSubscription = chatRepository.typingStream.listen((data) {
add(ChatEvent.typingUpdated(data['chatId'], data['userId'], data['isTyping']));
});
}
Future<void> _onStarted(ChatEventStarted event, Emitter<ChatState> emit) async {
// ignore: avoid_print
print('[DEBUG] ChatBloc._onStarted for user: ${event.userId}');
// Start SignalR in background
chatRepository.initSignalR().catchError((e) {
// Log or handle SignalR init error
});
// Immediately trigger chats loading
add(const ChatEvent.chatsLoaded());
add(ChatEvent.chatsLoaded(currentUserId: event.userId));
add(const ChatEvent.favoritesRequested());
}
Future<void> _onChatsLoaded(ChatEventChatsLoaded event, Emitter<ChatState> emit) async {
@@ -83,9 +93,42 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
);
}
Future<void> _onTypingUpdated(ChatEventTypingUpdated event, Emitter<ChatState> emit) async {
final Map<String, Set<String>> newTypingUsers = Map.from(state.typingUsers);
final Set<String> chatTypingUsers = Set.from(newTypingUsers[event.chatId] ?? {});
if (event.isTyping) {
chatTypingUsers.add(event.userId);
} else {
chatTypingUsers.remove(event.userId);
}
if (chatTypingUsers.isEmpty) {
newTypingUsers.remove(event.chatId);
} else {
newTypingUsers[event.chatId] = chatTypingUsers;
}
emit(state.copyWith(typingUsers: newTypingUsers));
}
Future<void> _onSendTypingStatus(ChatEventSendTypingStatus event, Emitter<ChatState> emit) async {
await chatRepository.sendTypingStatus(event.chatId, event.isTyping);
}
Future<void> _onCacheCleared(ChatEventCacheCleared event, Emitter<ChatState> emit) async {
// ignore: avoid_print
print('[DEBUG] ChatBloc._onCacheCleared START');
await chatRepository.clearCache();
// ignore: avoid_print
print('[DEBUG] ChatBloc._onCacheCleared END');
emit(const ChatState.initial());
}
@override
Future<void> close() {
_messageSubscription?.cancel();
_typingSubscription?.cancel();
chatRepository.disposeSignalR();
return super.close();
}
@@ -4,10 +4,13 @@ part 'chat_event.freezed.dart';
@freezed
class ChatEvent with _$ChatEvent {
const factory ChatEvent.started() = ChatEventStarted;
const factory ChatEvent.started({String? userId}) = ChatEventStarted;
const factory ChatEvent.chatsLoaded({String? currentUserId}) = ChatEventChatsLoaded;
const factory ChatEvent.chatSelected(String chatId) = ChatEventChatSelected;
const factory ChatEvent.messagesRequested(String chatId, {String? cursor}) = ChatEventMessagesRequested;
const factory ChatEvent.messageSent(String chatId, String content) = ChatEventMessageSent;
const factory ChatEvent.favoritesRequested() = ChatEventFavoritesRequested;
const factory ChatEvent.typingUpdated(String chatId, String userId, bool isTyping) = ChatEventTypingUpdated;
const factory ChatEvent.sendTypingStatus(String chatId, bool isTyping) = ChatEventSendTypingStatus;
const factory ChatEvent.cacheCleared() = ChatEventCacheCleared;
}
File diff suppressed because it is too large Load Diff
@@ -6,10 +6,10 @@ part 'chat_state.freezed.dart';
@freezed
class ChatState with _$ChatState {
const factory ChatState.initial() = ChatInitial;
const factory ChatState.loading() = ChatLoading;
const factory ChatState.chatsLoaded(List<Chat> chats) = _ChatsLoaded;
const factory ChatState.chatSelected(Chat chat, List<Message> messages) = _ChatSelected;
const factory ChatState.messagesLoaded(List<Message> messages) = _MessagesLoaded;
const factory ChatState.error(String message) = ChatError;
const factory ChatState.initial({@Default({}) Map<String, Set<String>> typingUsers}) = ChatInitial;
const factory ChatState.loading({@Default({}) Map<String, Set<String>> typingUsers}) = ChatLoading;
const factory ChatState.chatsLoaded(List<Chat> chats, {@Default({}) Map<String, Set<String>> typingUsers}) = _ChatsLoaded;
const factory ChatState.chatSelected(Chat chat, List<Message> messages, {@Default({}) Map<String, Set<String>> typingUsers}) = _ChatSelected;
const factory ChatState.messagesLoaded(List<Message> messages, {@Default({}) Map<String, Set<String>> typingUsers}) = _MessagesLoaded;
const factory ChatState.error(String message, {@Default({}) Map<String, Set<String>> typingUsers}) = ChatError;
}
File diff suppressed because it is too large Load Diff
@@ -35,84 +35,206 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
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: 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('Начните общение')),
);
},
return BlocBuilder<ChatBloc, ChatState>(
builder: (context, state) {
final currentChat = state.maybeWhen(
chatSelected: (chat, _, __) => chat,
orElse: () => widget.chat,
);
return Scaffold(
appBar: AppBar(
centerTitle: false,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(currentChat.type == 'favorites' ? AppLocalizations.of(context)!.favorites : currentChat.title),
if (currentChat.type != 'favorites')
_buildPresenceStatus(currentChat, state),
],
),
actions: [
IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}),
],
),
_buildMessageInput(),
],
),
body: Column(
children: [
Expanded(
child: 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(currentChat),
],
),
);
},
);
}
Widget _buildPresenceStatus(Chat chat, ChatState state) {
// Check typing status first
final typingInChat = state.typingUsers[chat.id];
final otherMemberTyping = typingInChat?.any((id) => id != widget.currentUserId) ?? false;
if (otherMemberTyping) {
return const Text(
'печатает...',
style: TextStyle(fontSize: 12, color: Colors.white70, fontWeight: FontWeight.normal, fontStyle: FontStyle.italic),
);
}
final participants = chat.participants;
if (participants == null || participants.isEmpty) {
return const SizedBox.shrink();
}
final otherMember = participants.firstWhere(
(m) => m.id != widget.currentUserId,
orElse: () => participants[0],
);
final l10n = AppLocalizations.of(context)!;
final locale = Localizations.localeOf(context).languageCode;
if (otherMember.isOnline == true) {
return Row(
children: [
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.greenAccent,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
Text(
l10n.online,
style: const TextStyle(fontSize: 12, color: Colors.white70, fontWeight: FontWeight.normal),
),
],
);
} else if (otherMember.lastSeen != null) {
final lastSeen = otherMember.lastSeen!.toLocal();
final timeStr = DateFormat.Hm(locale).format(lastSeen);
final dateStr = _formatDate(lastSeen, locale);
return Text(
l10n.lastSeen(dateStr, timeStr),
style: const TextStyle(fontSize: 12, color: Colors.white70, fontWeight: FontWeight.normal),
);
}
return const SizedBox.shrink();
}
String _formatDate(DateTime date, String locale) {
final now = DateTime.now();
final localDate = date.toLocal();
if (localDate.year == now.year) {
return DateFormat.MMMMd(locale).format(localDate);
}
return DateFormat.yMMMMd(locale).format(localDate);
}
Widget _buildMessageList(List<Message> messages) {
final l10n = AppLocalizations.of(context)!;
final locale = Localizations.localeOf(context).languageCode;
if (messages.isEmpty) {
return const Center(child: Text('Сообщений пока нет'));
return Center(child: Text(l10n.noMessages));
}
final List<dynamic> items = [];
for (int i = 0; i < messages.length; i++) {
final message = messages[i];
items.add(message);
if (message.createdAt != null) {
final localCreatedAt = message.createdAt!.toLocal();
final currentDay = DateTime(localCreatedAt.year, localCreatedAt.month, localCreatedAt.day);
if (i == messages.length - 1) {
items.add(currentDay);
} else {
final nextMessage = messages[i + 1];
if (nextMessage.createdAt != null) {
final nextLocalCreatedAt = nextMessage.createdAt!.toLocal();
final nextDay = DateTime(nextLocalCreatedAt.year, nextLocalCreatedAt.month, nextLocalCreatedAt.day);
if (currentDay != nextDay) {
items.add(currentDay);
}
}
}
}
}
return ListView.builder(
controller: _scrollController,
reverse: true,
padding: const EdgeInsets.all(16),
itemCount: messages.length,
itemCount: items.length,
itemBuilder: (context, index) {
final message = messages[index];
final item = items[index];
if (item is DateTime) {
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 16),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
_formatDate(item, locale),
style: const TextStyle(fontSize: 12, color: Colors.black54),
),
),
);
}
final message = item as Message;
final isMe = message.senderId == widget.currentUserId;
return Align(
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4),
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 8),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.75),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: isMe ? Theme.of(context).primaryColor : Colors.grey[200],
color: isMe ? const Color(0xFFE3F2FD) : Colors.white,
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),
bottomRight: isMe ? const Radius.circular(2) : const Radius.circular(16),
bottomLeft: !isMe ? const Radius.circular(2) : const Radius.circular(16),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Text(
message.content,
style: TextStyle(color: isMe ? Colors.white : Colors.black87),
style: const TextStyle(color: Colors.black87, fontSize: 16),
),
const SizedBox(height: 4),
const SizedBox(height: 2),
if (message.createdAt != null)
Text(
DateFormat.Hm().format(message.createdAt!),
style: TextStyle(
DateFormat.Hm(locale).format(message.createdAt!.toLocal()),
style: const TextStyle(
fontSize: 10,
color: isMe ? Colors.white70 : Colors.black45,
color: Colors.black45,
),
),
],
@@ -123,7 +245,7 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
);
}
Widget _buildMessageInput() {
Widget _buildMessageInput(Chat chat) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
@@ -143,16 +265,19 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
Expanded(
child: TextField(
controller: _messageController,
decoration: const InputDecoration(
hintText: 'Сообщение',
decoration: InputDecoration(
hintText: AppLocalizations.of(context)!.messageHint,
border: InputBorder.none,
),
onSubmitted: (_) => _sendMessage(),
onChanged: (value) {
context.read<ChatBloc>().add(ChatEvent.sendTypingStatus(chat.id, value.isNotEmpty));
},
onSubmitted: (_) => _sendMessage(chat.id),
),
),
IconButton(
icon: const Icon(Icons.send),
onPressed: _sendMessage,
onPressed: () => _sendMessage(chat.id),
color: Theme.of(context).primaryColor,
),
],
@@ -161,10 +286,11 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
);
}
void _sendMessage() {
void _sendMessage(String chatId) {
final content = _messageController.text.trim();
if (content.isNotEmpty) {
context.read<ChatBloc>().add(ChatEvent.messageSent(widget.chat.id, content));
context.read<ChatBloc>().add(ChatEvent.messageSent(chatId, content));
context.read<ChatBloc>().add(ChatEvent.sendTypingStatus(chatId, false));
_messageController.clear();
}
}
@@ -47,7 +47,7 @@ class _ChatsPageState extends State<ChatsPage> {
body: BlocConsumer<ChatBloc, ChatState>(
listener: (context, state) {
state.whenOrNull(
chatSelected: (chat, _) {
chatSelected: (chat, _, __) {
final authState = context.read<AuthBloc>().state;
final userId = authState.maybeWhen(
authenticated: (id) => id,
@@ -72,9 +72,9 @@ class _ChatsPageState extends State<ChatsPage> {
},
builder: (context, state) {
return state.maybeWhen(
loading: () => const Center(child: CircularProgressIndicator()),
chatsLoaded: (chats) => _buildChatList(chats, l10n),
error: (message) => Center(child: Text('${l10n.error}: $message')),
loading: (_) => const Center(child: CircularProgressIndicator()),
chatsLoaded: (chats, _) => _buildChatList(chats, l10n),
error: (message, _) => Center(child: Text('${l10n.error}: $message')),
orElse: () => const Center(child: CircularProgressIndicator()),
);
},
@@ -141,7 +141,7 @@ class _ChatsPageState extends State<ChatsPage> {
children: [
if (chat.lastMessageTime != null)
Text(
DateFormat.Hm().format(chat.lastMessageTime!),
DateFormat.Hm().format(chat.lastMessageTime!.toLocal()),
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 4),