Повторный вход в аккаунт, очистка кэша
This commit is contained in:
@@ -80,5 +80,9 @@
|
|||||||
"enterUsername": "Enter login",
|
"enterUsername": "Enter login",
|
||||||
"invalidUsername": "Login must be at least 3 characters and without Cyrillic",
|
"invalidUsername": "Login must be at least 3 characters and without Cyrillic",
|
||||||
"loginFailed": "Login failed. Check your login and password",
|
"loginFailed": "Login failed. Check your login and password",
|
||||||
"registrationFailed": "Registration failed"
|
"registrationFailed": "Registration failed",
|
||||||
|
"online": "online",
|
||||||
|
"lastSeen": "last seen {date} at {time}",
|
||||||
|
"noMessages": "No messages yet",
|
||||||
|
"messageHint": "Message"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,5 +80,9 @@
|
|||||||
"enterUsername": "Введите логин",
|
"enterUsername": "Введите логин",
|
||||||
"invalidUsername": "Логин должен быть от 3 символов и без кириллицы",
|
"invalidUsername": "Логин должен быть от 3 символов и без кириллицы",
|
||||||
"loginFailed": "Ошибка входа. Проверьте логин и пароль",
|
"loginFailed": "Ошибка входа. Проверьте логин и пароль",
|
||||||
"registrationFailed": "Ошибка регистрации"
|
"registrationFailed": "Ошибка регистрации",
|
||||||
|
"online": "в сети",
|
||||||
|
"lastSeen": "был(а) в сети {date} в {time}",
|
||||||
|
"noMessages": "Сообщений пока нет",
|
||||||
|
"messageHint": "Сообщение"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,13 +56,26 @@ class MessengerApp extends StatelessWidget {
|
|||||||
GlobalWidgetsLocalizations.delegate,
|
GlobalWidgetsLocalizations.delegate,
|
||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
],
|
],
|
||||||
home: BlocBuilder<AuthBloc, AuthState>(
|
home: BlocListener<AuthBloc, AuthState>(
|
||||||
builder: (context, authState) {
|
listener: (context, authState) {
|
||||||
return authState.maybeWhen(
|
authState.maybeWhen(
|
||||||
authenticated: (_) => const MainScreen(),
|
authenticated: (userId) {
|
||||||
orElse: () => const LoginPage(),
|
context.read<ChatBloc>().add(ChatEvent.started(userId: userId));
|
||||||
|
},
|
||||||
|
unauthenticated: () {
|
||||||
|
context.read<ChatBloc>().add(const ChatEvent.cacheCleared());
|
||||||
|
},
|
||||||
|
orElse: () {},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
child: BlocBuilder<AuthBloc, AuthState>(
|
||||||
|
builder: (context, authState) {
|
||||||
|
return authState.maybeWhen(
|
||||||
|
authenticated: (_) => const MainScreen(),
|
||||||
|
orElse: () => const LoginPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ class SignalRService {
|
|||||||
SignalRService(this.prefs);
|
SignalRService(this.prefs);
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
|
if (_hubConnection != null) {
|
||||||
|
await stop();
|
||||||
|
}
|
||||||
final apiUrl = prefs.getString('api_url') ?? 'https://api.messenger.app';
|
final apiUrl = prefs.getString('api_url') ?? 'https://api.messenger.app';
|
||||||
final token = prefs.getString('access_token');
|
final token = prefs.getString('access_token');
|
||||||
|
|
||||||
@@ -33,12 +36,38 @@ class SignalRService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
_hubConnection?.on('ReceiveMessage', _handleReceiveMessage);
|
_hubConnection?.on('ReceiveMessage', _handleReceiveMessage);
|
||||||
|
_hubConnection?.on('user_typing', _handleUserTyping);
|
||||||
|
_hubConnection?.on('user_stopped_typing', _handleUserStoppedTyping);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _hubConnection?.start();
|
await _hubConnection?.start();
|
||||||
// print('[SignalR] Connection started');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// print('[SignalR] Error starting connection: $e');
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final _typingController = StreamController<Map<String, dynamic>>.broadcast();
|
||||||
|
Stream<Map<String, dynamic>> get typingUpdates => _typingController.stream;
|
||||||
|
|
||||||
|
void _handleUserTyping(List<dynamic>? arguments) {
|
||||||
|
if (arguments != null && arguments.isNotEmpty) {
|
||||||
|
final data = arguments[0] as Map<String, dynamic>;
|
||||||
|
_typingController.add({'chatId': data['ChatId'] ?? data['chatId'], 'userId': data['UserId'] ?? data['userId'], 'isTyping': true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleUserStoppedTyping(List<dynamic>? arguments) {
|
||||||
|
if (arguments != null && arguments.isNotEmpty) {
|
||||||
|
final data = arguments[0] as Map<String, dynamic>;
|
||||||
|
_typingController.add({'chatId': data['ChatId'] ?? data['chatId'], 'userId': data['UserId'] ?? data['userId'], 'isTyping': false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> sendTypingStatus(String chatId, bool isTyping) async {
|
||||||
|
if (isTyping) {
|
||||||
|
await _hubConnection?.invoke('typing_start', args: [chatId]);
|
||||||
|
} else {
|
||||||
|
await _hubConnection?.invoke('typing_stop', args: [chatId]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ class AuthLocalDataSourceImpl implements AuthLocalDataSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> saveToken(String token) async {
|
Future<void> saveToken(String token) async {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[DEBUG] Saving new access token (starts with: ${token.substring(0, 10)}...)');
|
||||||
await sharedPreferences.setString(_accessTokenKey, token);
|
await sharedPreferences.setString(_accessTokenKey, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,10 +90,16 @@ class ChatLocalDataSourceImpl implements ChatLocalDataSource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<void>> clearCache() async {
|
Future<Result<void>> clearCache() async {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[DEBUG] ChatLocalDataSource.clearCache calling isar.clear()');
|
||||||
try {
|
try {
|
||||||
await isar.writeTxn(() => isar.clear());
|
await isar.writeTxn(() => isar.clear());
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[DEBUG] ChatLocalDataSource.clearCache SUCCESS');
|
||||||
return const Result.success(null);
|
return const Result.success(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[DEBUG] ChatLocalDataSource.clearCache ERROR: $e');
|
||||||
return Result.failure(AppError.database(message: e.toString()));
|
return Result.failure(AppError.database(message: e.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,16 +137,23 @@ class ChatRemoteDataSourceImpl implements ChatRemoteDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Chat _mapChatJson(Map<String, dynamic> json, {String? currentUserId}) {
|
Chat _mapChatJson(Map<String, dynamic> json, {String? currentUserId}) {
|
||||||
final members = (json['members'] as List<dynamic>?)?.map((m) {
|
final membersData = json['members'] ?? json['participants'];
|
||||||
final userData = m['user'] as Map<String, dynamic>?;
|
final members = (membersData as List<dynamic>?)?.map((m) {
|
||||||
|
final userData = m is Map<String, dynamic> ? (m['user'] ?? m) : m;
|
||||||
return User(
|
return User(
|
||||||
id: m['userId'] ?? (userData?['id'] ?? ''),
|
id: (m is Map ? m['userId'] : null) ?? (userData?['id'] ?? ''),
|
||||||
name: userData?['displayName'] ?? '',
|
name: userData?['displayName'] ?? (userData?['name'] ?? ''),
|
||||||
avatarUrl: userData?['avatar'],
|
avatarUrl: userData?['avatar'] ?? userData?['avatarUrl'],
|
||||||
|
isOnline: userData?['isOnline'] ?? userData?['IsOnline'] ?? false,
|
||||||
|
lastSeen: _parseDateTime(userData?['lastSeen'] ?? userData?['LastSeen']),
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
final lastMsgJson = json['lastMessage'] ?? (json['messages'] as List?)?.firstOrNull;
|
dynamic lastMsgRaw = json['lastMessage'];
|
||||||
|
if (lastMsgRaw is List && lastMsgRaw.isNotEmpty) {
|
||||||
|
lastMsgRaw = lastMsgRaw.first;
|
||||||
|
}
|
||||||
|
final lastMsgJson = lastMsgRaw ?? (json['messages'] as List?)?.firstOrNull;
|
||||||
final type = json['type']?.toString().toLowerCase() ?? 'personal';
|
final type = json['type']?.toString().toLowerCase() ?? 'personal';
|
||||||
|
|
||||||
String title = json['name'] ?? '';
|
String title = json['name'] ?? '';
|
||||||
@@ -171,21 +178,36 @@ class ChatRemoteDataSourceImpl implements ChatRemoteDataSource {
|
|||||||
participants: members,
|
participants: members,
|
||||||
unreadCount: json['unreadCount'] ?? 0,
|
unreadCount: json['unreadCount'] ?? 0,
|
||||||
isPinned: json['isPinned'] ?? false,
|
isPinned: json['isPinned'] ?? false,
|
||||||
lastMessage: lastMsgJson != null ? _mapMessageJson(lastMsgJson) : null,
|
lastMessage: lastMsgJson != null && lastMsgJson is Map<String, dynamic>
|
||||||
lastMessageTime: lastMsgJson != null ? DateTime.parse(lastMsgJson['createdAt']) : null,
|
? _mapMessageJson(lastMsgJson)
|
||||||
|
: null,
|
||||||
|
lastMessageTime: lastMsgJson != null && lastMsgJson is Map<String, dynamic>
|
||||||
|
? _parseDateTime(lastMsgJson['createdAt'])
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Message _mapMessageJson(Map<String, dynamic> json) {
|
Message _mapMessageJson(Map<String, dynamic> json) {
|
||||||
return Message(
|
return Message(
|
||||||
id: json['id'],
|
id: json['id'] ?? '',
|
||||||
chatId: json['chatId'],
|
chatId: json['chatId'] ?? '',
|
||||||
senderId: json['senderId'],
|
senderId: json['userId'] ?? (json['senderId'] ?? ''),
|
||||||
content: json['content'] ?? '',
|
content: json['content'] ?? '',
|
||||||
messageType: json['type'] ?? 'Text',
|
messageType: json['type']?.toString().toLowerCase() ?? 'text',
|
||||||
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null,
|
createdAt: _parseDateTime(json['createdAt']),
|
||||||
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt']) : null,
|
updatedAt: _parseDateTime(json['updatedAt']),
|
||||||
isRead: (json['readBy'] as List?)?.isNotEmpty ?? false,
|
isRead: json['isRead'] ?? false,
|
||||||
|
media: json['media'] is Map<String, dynamic> ? json['media'] as Map<String, dynamic> : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DateTime? _parseDateTime(String? dateStr) {
|
||||||
|
if (dateStr == null) return null;
|
||||||
|
// If it doesn't have a timezone indicator, assume it's UTC
|
||||||
|
String normalized = dateStr;
|
||||||
|
if (!normalized.endsWith('Z') && !normalized.contains('+')) {
|
||||||
|
normalized += 'Z';
|
||||||
|
}
|
||||||
|
return DateTime.parse(normalized).toLocal();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ class ChatRepositoryImpl implements ChatRepository {
|
|||||||
@override
|
@override
|
||||||
Stream<Message> get messageStream => signalRService.messages;
|
Stream<Message> get messageStream => signalRService.messages;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Map<String, dynamic>> get typingStream => signalRService.typingUpdates;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> sendTypingStatus(String chatId, bool isTyping) => signalRService.sendTypingStatus(chatId, isTyping);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> initSignalR() => signalRService.init();
|
Future<void> initSignalR() => signalRService.init();
|
||||||
|
|
||||||
@@ -29,6 +35,8 @@ class ChatRepositoryImpl implements ChatRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<List<Chat>>> getChats({String? currentUserId}) async {
|
Future<Result<List<Chat>>> getChats({String? currentUserId}) async {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[DEBUG] ChatRepository.getChats for user: $currentUserId');
|
||||||
// 1. Return cached chats immediately if available
|
// 1. Return cached chats immediately if available
|
||||||
final cachedResult = await localDataSource.getCachedChats();
|
final cachedResult = await localDataSource.getCachedChats();
|
||||||
|
|
||||||
@@ -36,10 +44,14 @@ class ChatRepositoryImpl implements ChatRepository {
|
|||||||
final remoteResult = await remoteDataSource.getChats(currentUserId: currentUserId);
|
final remoteResult = await remoteDataSource.getChats(currentUserId: currentUserId);
|
||||||
if (remoteResult.isSuccess) {
|
if (remoteResult.isSuccess) {
|
||||||
final chats = remoteResult.data!;
|
final chats = remoteResult.data!;
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[DEBUG] Remote getChats success: ${chats.length} chats');
|
||||||
await localDataSource.cacheChats(chats);
|
await localDataSource.cacheChats(chats);
|
||||||
return Result.success(chats);
|
return Result.success(chats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[DEBUG] Remote getChats failed or empty');
|
||||||
// If remote fails, return cached or error
|
// If remote fails, return cached or error
|
||||||
return cachedResult;
|
return cachedResult;
|
||||||
}
|
}
|
||||||
@@ -97,13 +109,19 @@ class ChatRepositoryImpl implements ChatRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<List<Message>>> getMessages(String chatId, {String? cursor}) async {
|
Future<Result<List<Message>>> getMessages(String chatId, {String? cursor}) async {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[DEBUG] ChatRepository.getMessages for chat: $chatId');
|
||||||
if (cursor == null) {
|
if (cursor == null) {
|
||||||
final cached = await localDataSource.getMessagesFromCache(chatId);
|
final cached = await localDataSource.getMessagesFromCache(chatId);
|
||||||
final remote = await remoteDataSource.getMessages(chatId);
|
final remote = await remoteDataSource.getMessages(chatId);
|
||||||
if (remote.isSuccess) {
|
if (remote.isSuccess) {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[DEBUG] Remote getMessages success: ${remote.data!.length} messages');
|
||||||
await localDataSource.cacheMessages(chatId, remote.data!);
|
await localDataSource.cacheMessages(chatId, remote.data!);
|
||||||
return remote;
|
return remote;
|
||||||
}
|
}
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[DEBUG] Remote getMessages failed: ${remote.failure?.message}');
|
||||||
return cached;
|
return cached;
|
||||||
} else {
|
} else {
|
||||||
return remoteDataSource.getMessages(chatId, cursor: cursor);
|
return remoteDataSource.getMessages(chatId, cursor: cursor);
|
||||||
@@ -130,4 +148,7 @@ class ChatRepositoryImpl implements ChatRepository {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<void>> clearCache() => localDataSource.clearCache();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,11 @@ abstract class ChatRepository {
|
|||||||
Future<Result<List<Message>>> getMessages(String chatId, {String? cursor});
|
Future<Result<List<Message>>> getMessages(String chatId, {String? cursor});
|
||||||
Future<Result<void>> togglePin(String chatId);
|
Future<Result<void>> togglePin(String chatId);
|
||||||
Future<Result<void>> clearChat(String chatId);
|
Future<Result<void>> clearChat(String chatId);
|
||||||
|
Future<Result<void>> clearCache();
|
||||||
|
|
||||||
Stream<Message> get messageStream;
|
Stream<Message> get messageStream;
|
||||||
|
Stream<Map<String, dynamic>> get typingStream;
|
||||||
|
Future<void> sendTypingStatus(String chatId, bool isTyping);
|
||||||
Future<void> initSignalR();
|
Future<void> initSignalR();
|
||||||
Future<void> disposeSignalR();
|
Future<void> disposeSignalR();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'chat_state.dart';
|
|||||||
class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||||
final ChatRepository chatRepository;
|
final ChatRepository chatRepository;
|
||||||
StreamSubscription? _messageSubscription;
|
StreamSubscription? _messageSubscription;
|
||||||
|
StreamSubscription? _typingSubscription;
|
||||||
|
|
||||||
ChatBloc({required this.chatRepository}) : super(const ChatState.initial()) {
|
ChatBloc({required this.chatRepository}) : super(const ChatState.initial()) {
|
||||||
on<ChatEventStarted>(_onStarted);
|
on<ChatEventStarted>(_onStarted);
|
||||||
@@ -17,20 +18,29 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
on<ChatEventMessagesRequested>(_onMessagesRequested);
|
on<ChatEventMessagesRequested>(_onMessagesRequested);
|
||||||
on<ChatEventMessageSent>(_onMessageSent);
|
on<ChatEventMessageSent>(_onMessageSent);
|
||||||
on<ChatEventFavoritesRequested>(_onFavoritesRequested);
|
on<ChatEventFavoritesRequested>(_onFavoritesRequested);
|
||||||
|
on<ChatEventTypingUpdated>(_onTypingUpdated);
|
||||||
|
on<ChatEventSendTypingStatus>(_onSendTypingStatus);
|
||||||
|
on<ChatEventCacheCleared>(_onCacheCleared);
|
||||||
|
|
||||||
_messageSubscription = chatRepository.messageStream.listen((message) {
|
_messageSubscription = chatRepository.messageStream.listen((message) {
|
||||||
// Handle real-time messages
|
|
||||||
add(ChatEvent.messagesRequested(message.chatId));
|
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 {
|
Future<void> _onStarted(ChatEventStarted event, Emitter<ChatState> emit) async {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[DEBUG] ChatBloc._onStarted for user: ${event.userId}');
|
||||||
// Start SignalR in background
|
// Start SignalR in background
|
||||||
chatRepository.initSignalR().catchError((e) {
|
chatRepository.initSignalR().catchError((e) {
|
||||||
// Log or handle SignalR init error
|
// Log or handle SignalR init error
|
||||||
});
|
});
|
||||||
// Immediately trigger chats loading
|
// 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 {
|
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
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
_messageSubscription?.cancel();
|
_messageSubscription?.cancel();
|
||||||
|
_typingSubscription?.cancel();
|
||||||
chatRepository.disposeSignalR();
|
chatRepository.disposeSignalR();
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ part 'chat_event.freezed.dart';
|
|||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class ChatEvent with _$ChatEvent {
|
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.chatsLoaded({String? currentUserId}) = ChatEventChatsLoaded;
|
||||||
const factory ChatEvent.chatSelected(String chatId) = ChatEventChatSelected;
|
const factory ChatEvent.chatSelected(String chatId) = ChatEventChatSelected;
|
||||||
const factory ChatEvent.messagesRequested(String chatId, {String? cursor}) = ChatEventMessagesRequested;
|
const factory ChatEvent.messagesRequested(String chatId, {String? cursor}) = ChatEventMessagesRequested;
|
||||||
const factory ChatEvent.messageSent(String chatId, String content) = ChatEventMessageSent;
|
const factory ChatEvent.messageSent(String chatId, String content) = ChatEventMessageSent;
|
||||||
const factory ChatEvent.favoritesRequested() = ChatEventFavoritesRequested;
|
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
|
@freezed
|
||||||
class ChatState with _$ChatState {
|
class ChatState with _$ChatState {
|
||||||
const factory ChatState.initial() = ChatInitial;
|
const factory ChatState.initial({@Default({}) Map<String, Set<String>> typingUsers}) = ChatInitial;
|
||||||
const factory ChatState.loading() = ChatLoading;
|
const factory ChatState.loading({@Default({}) Map<String, Set<String>> typingUsers}) = ChatLoading;
|
||||||
const factory ChatState.chatsLoaded(List<Chat> chats) = _ChatsLoaded;
|
const factory ChatState.chatsLoaded(List<Chat> chats, {@Default({}) Map<String, Set<String>> typingUsers}) = _ChatsLoaded;
|
||||||
const factory ChatState.chatSelected(Chat chat, List<Message> messages) = _ChatSelected;
|
const factory ChatState.chatSelected(Chat chat, List<Message> messages, {@Default({}) Map<String, Set<String>> typingUsers}) = _ChatSelected;
|
||||||
const factory ChatState.messagesLoaded(List<Message> messages) = _MessagesLoaded;
|
const factory ChatState.messagesLoaded(List<Message> messages, {@Default({}) Map<String, Set<String>> typingUsers}) = _MessagesLoaded;
|
||||||
const factory ChatState.error(String message) = ChatError;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return BlocBuilder<ChatBloc, ChatState>(
|
||||||
appBar: AppBar(
|
builder: (context, state) {
|
||||||
title: Column(
|
final currentChat = state.maybeWhen(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
chatSelected: (chat, _, __) => chat,
|
||||||
children: [
|
orElse: () => widget.chat,
|
||||||
Text(widget.chat.type == 'favorites' ? AppLocalizations.of(context)!.favorites : widget.chat.title),
|
);
|
||||||
if (widget.chat.type != 'favorites')
|
|
||||||
Text(
|
return Scaffold(
|
||||||
AppLocalizations.of(context)!.connected,
|
appBar: AppBar(
|
||||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
|
centerTitle: false,
|
||||||
),
|
title: Column(
|
||||||
],
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
children: [
|
||||||
actions: [
|
Text(currentChat.type == 'favorites' ? AppLocalizations.of(context)!.favorites : currentChat.title),
|
||||||
IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}),
|
if (currentChat.type != 'favorites')
|
||||||
],
|
_buildPresenceStatus(currentChat, state),
|
||||||
),
|
],
|
||||||
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('Начните общение')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
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) {
|
Widget _buildMessageList(List<Message> messages) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final locale = Localizations.localeOf(context).languageCode;
|
||||||
|
|
||||||
if (messages.isEmpty) {
|
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(
|
return ListView.builder(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
reverse: true,
|
reverse: true,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
itemCount: messages.length,
|
itemCount: items.length,
|
||||||
itemBuilder: (context, index) {
|
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;
|
final isMe = message.senderId == widget.currentUserId;
|
||||||
|
|
||||||
return Align(
|
return Align(
|
||||||
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
|
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
|
||||||
child: Container(
|
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),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isMe ? Theme.of(context).primaryColor : Colors.grey[200],
|
color: isMe ? const Color(0xFFE3F2FD) : Colors.white,
|
||||||
borderRadius: BorderRadius.circular(16).copyWith(
|
borderRadius: BorderRadius.circular(16).copyWith(
|
||||||
bottomRight: isMe ? const Radius.circular(0) : const Radius.circular(16),
|
bottomRight: isMe ? const Radius.circular(2) : const Radius.circular(16),
|
||||||
bottomLeft: !isMe ? const Radius.circular(0) : 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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
message.content,
|
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)
|
if (message.createdAt != null)
|
||||||
Text(
|
Text(
|
||||||
DateFormat.Hm().format(message.createdAt!),
|
DateFormat.Hm(locale).format(message.createdAt!.toLocal()),
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 10,
|
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(
|
return Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -143,16 +265,19 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _messageController,
|
controller: _messageController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Сообщение',
|
hintText: AppLocalizations.of(context)!.messageHint,
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
onSubmitted: (_) => _sendMessage(),
|
onChanged: (value) {
|
||||||
|
context.read<ChatBloc>().add(ChatEvent.sendTypingStatus(chat.id, value.isNotEmpty));
|
||||||
|
},
|
||||||
|
onSubmitted: (_) => _sendMessage(chat.id),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.send),
|
icon: const Icon(Icons.send),
|
||||||
onPressed: _sendMessage,
|
onPressed: () => _sendMessage(chat.id),
|
||||||
color: Theme.of(context).primaryColor,
|
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();
|
final content = _messageController.text.trim();
|
||||||
if (content.isNotEmpty) {
|
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();
|
_messageController.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class _ChatsPageState extends State<ChatsPage> {
|
|||||||
body: BlocConsumer<ChatBloc, ChatState>(
|
body: BlocConsumer<ChatBloc, ChatState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
state.whenOrNull(
|
state.whenOrNull(
|
||||||
chatSelected: (chat, _) {
|
chatSelected: (chat, _, __) {
|
||||||
final authState = context.read<AuthBloc>().state;
|
final authState = context.read<AuthBloc>().state;
|
||||||
final userId = authState.maybeWhen(
|
final userId = authState.maybeWhen(
|
||||||
authenticated: (id) => id,
|
authenticated: (id) => id,
|
||||||
@@ -72,9 +72,9 @@ class _ChatsPageState extends State<ChatsPage> {
|
|||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return state.maybeWhen(
|
return state.maybeWhen(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: (_) => const Center(child: CircularProgressIndicator()),
|
||||||
chatsLoaded: (chats) => _buildChatList(chats, l10n),
|
chatsLoaded: (chats, _) => _buildChatList(chats, l10n),
|
||||||
error: (message) => Center(child: Text('${l10n.error}: $message')),
|
error: (message, _) => Center(child: Text('${l10n.error}: $message')),
|
||||||
orElse: () => const Center(child: CircularProgressIndicator()),
|
orElse: () => const Center(child: CircularProgressIndicator()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -141,7 +141,7 @@ class _ChatsPageState extends State<ChatsPage> {
|
|||||||
children: [
|
children: [
|
||||||
if (chat.lastMessageTime != null)
|
if (chat.lastMessageTime != null)
|
||||||
Text(
|
Text(
|
||||||
DateFormat.Hm().format(chat.lastMessageTime!),
|
DateFormat.Hm().format(chat.lastMessageTime!.toLocal()),
|
||||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ Future<void> initDependencies() async {
|
|||||||
dio.interceptors.add(InterceptorsWrapper(
|
dio.interceptors.add(InterceptorsWrapper(
|
||||||
onRequest: (options, handler) {
|
onRequest: (options, handler) {
|
||||||
final token = prefs.getString('access_token');
|
final token = prefs.getString('access_token');
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[DEBUG] Dio Request: ${options.path}, Token present: ${token != null}');
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
options.headers['Authorization'] = 'Bearer $token';
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -583,6 +583,30 @@ abstract class AppLocalizations {
|
|||||||
/// In ru, this message translates to:
|
/// In ru, this message translates to:
|
||||||
/// **'Ошибка регистрации'**
|
/// **'Ошибка регистрации'**
|
||||||
String get registrationFailed;
|
String get registrationFailed;
|
||||||
|
|
||||||
|
/// No description provided for @online.
|
||||||
|
///
|
||||||
|
/// In ru, this message translates to:
|
||||||
|
/// **'в сети'**
|
||||||
|
String get online;
|
||||||
|
|
||||||
|
/// No description provided for @lastSeen.
|
||||||
|
///
|
||||||
|
/// In ru, this message translates to:
|
||||||
|
/// **'был(а) в сети {date} в {time}'**
|
||||||
|
String lastSeen(Object date, Object time);
|
||||||
|
|
||||||
|
/// No description provided for @noMessages.
|
||||||
|
///
|
||||||
|
/// In ru, this message translates to:
|
||||||
|
/// **'Сообщений пока нет'**
|
||||||
|
String get noMessages;
|
||||||
|
|
||||||
|
/// No description provided for @messageHint.
|
||||||
|
///
|
||||||
|
/// In ru, this message translates to:
|
||||||
|
/// **'Сообщение'**
|
||||||
|
String get messageHint;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -251,4 +251,18 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get registrationFailed => 'Registration failed';
|
String get registrationFailed => 'Registration failed';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get online => 'online';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String lastSeen(Object date, Object time) {
|
||||||
|
return 'last seen $date at $time';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noMessages => 'No messages yet';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get messageHint => 'Message';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,4 +251,18 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get registrationFailed => 'Ошибка регистрации';
|
String get registrationFailed => 'Ошибка регистрации';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get online => 'в сети';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String lastSeen(Object date, Object time) {
|
||||||
|
return 'был(а) в сети $date в $time';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noMessages => 'Сообщений пока нет';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get messageHint => 'Сообщение';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'internal/di/injection_container.dart' as di;
|
import 'internal/di/injection_container.dart' as di;
|
||||||
import 'app.dart';
|
import 'app.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// Initialize date formatting for Russian and English
|
||||||
|
await initializeDateFormatting('ru', null);
|
||||||
|
await initializeDateFormatting('en', null);
|
||||||
|
|
||||||
// Set preferred orientations
|
// Set preferred orientations
|
||||||
await SystemChrome.setPreferredOrientations([
|
await SystemChrome.setPreferredOrientations([
|
||||||
DeviceOrientation.portraitUp,
|
DeviceOrientation.portraitUp,
|
||||||
|
|||||||
Reference in New Issue
Block a user