Merge remote-tracking branch 'upstream/main'

This commit is contained in:
misha
2026-02-10 12:38:08 +03:00
88 changed files with 1161 additions and 610 deletions

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
import 'package:eatvio_app/core/comments/services/comments_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CommentsRepository {
final CommentsService service;
@@ -9,7 +10,8 @@ class CommentsRepository {
Future<void> deleteComment({required int id}) async {
try {
return service.deleteComment(id);
} catch (e) {
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}
@@ -17,7 +19,8 @@ class CommentsRepository {
Future<void> updateComment({required int id, required String body}) async {
try {
return service.updateComment(id, CommentUpdateModel(body: body));
} catch (e) {
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}

View File

@@ -69,4 +69,9 @@ class AppConstants {
static const String usersEndpoint = '/users';
static const String postsEndpoint = '/posts';
static const String commentsEndpoint = '/comments';
// Delay of pending requests
/// 30 секунд стоит потому что запрос может идти 12-15 секунд
/// Особенно если производится отладка на сервере разработчика
static const Duration pendingDelay = Duration(seconds: 30);
}

View File

@@ -1,10 +1,12 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/constants/app_constants.dart';
import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart';
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sentry_dio/sentry_dio.dart';
/// Регистрация внешних зависимостей в системе внедрения зависимостей
class ExternalRegistrations {
@@ -19,15 +21,16 @@ class ExternalRegistrations {
/// Регистрация Dio HTTP клиента
static void _registerDio() {
sl.registerLazySingleton(
() =>
Dio(BaseOptions(
baseUrl: sl<AppConfig>().baseUrl,
headers: {'accept': 'application/json', 'content-type': 'application/json'},
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
sendTimeout: const Duration(seconds: 10),
))
..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]),
() => Dio(
BaseOptions(
baseUrl: sl<AppConfig>().baseUrl,
headers: {'accept': 'application/json', 'content-type': 'application/json'},
connectTimeout: AppConstants.pendingDelay,
receiveTimeout: AppConstants.pendingDelay,
sendTimeout: AppConstants.pendingDelay,
),
)..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()])
..addSentry(),
);
}

View File

@@ -12,6 +12,7 @@ import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/services.dart' show MethodChannel, PlatformException;
import 'package:sentry_flutter/sentry_flutter.dart';
class FirebaseApi {
final _firebaseMessaging = FirebaseMessaging.instance;
@@ -49,7 +50,7 @@ class FirebaseApi {
await sl<Dio>().post(
'/api/anonymous_token',
data: {
'platform': Platform.isIOS ? 'ios' : 'android',
'platform': Platform.isIOS ? 'ios' : (await _isHuaweiDevice()) ? 'huawei' : 'android',
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': await getToken(),
},
@@ -61,6 +62,7 @@ class FirebaseApi {
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
// Не пробрасываем ошибку дальше, так как это не критичная операция
}
}
@@ -85,6 +87,7 @@ class FirebaseApi {
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
// Не пробрасываем ошибку дальше, так как это не критичная операция
}
}
@@ -110,10 +113,12 @@ class FirebaseApi {
} on PlatformException catch (e, st) {
showErrorToast('$e $st');
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
return null;
} catch (e, st) {
showErrorToast('$e $st');
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
return null;
}
}

View File

@@ -1,6 +1,8 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:go_router/go_router.dart';
class TokenInterceptor extends Interceptor {
@@ -23,6 +25,7 @@ class TokenInterceptor extends Interceptor {
ErrorInterceptorHandler handler,
) async {
if (err.response?.statusCode == 401) {
await sl<WsManager>().clear();
await TokenStorage.removeTokenInStorage();
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
}

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
/// Helper class for managing themes and colors.
class ThemeHelper {
@@ -34,7 +34,8 @@ class ThemeHelper {
var colorScheme = _supportedColorScheme[_appTheme] ?? ColorSchemes.primaryColorScheme;
return ThemeData(
textTheme: GoogleFonts.mulishTextTheme(),
fontFamily: 'Mulish-Local',
textTheme: ThemeData.light().textTheme.apply(fontFamily: 'Mulish-Local'),
visualDensity: VisualDensity.standard,
colorScheme: colorScheme,
scaffoldBackgroundColor: colorScheme.onPrimary,

View File

@@ -16,6 +16,21 @@ class ValidationUtils {
return regex.hasMatch(phone);
}
/// Нормализация номера телефона (+79991234455)
static String normalizePhone(String phone) {
if (phone.isEmpty) return '';
final digits = phone.replaceAll(RegExp(r'[^\d]'), '');
if (digits.isEmpty) return '';
String normalized = digits;
if (digits.startsWith('8')) {
normalized = '7${digits.substring(1)}';
} else if (digits.length == 10) {
normalized = '7$digits';
}
return '+$normalized';
}
/// Валидация URL
static bool isValidUrl(String url) {
if (url.isEmpty) return false;

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/core/views/service/views_service.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ViewsRepository {
final ViewsService viewsService;
@@ -9,7 +10,8 @@ class ViewsRepository {
Future<void> viewContent({required ViewModel viewModel}) async {
try {
await viewsService.viewContent(viewModel: viewModel);
} catch (e) {
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
@@ -8,6 +9,7 @@ import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/websockets/ws_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
class WsManager {
@@ -22,6 +24,10 @@ class WsManager {
PrivateChannel? _privateMessagesChannel;
PrivateChannel? _privateDirectChatChannel;
StreamSubscription? _lifecycleSubscription;
StreamSubscription? _eventSubscription;
StreamSubscription? _connectionEstablishedSubscription;
Stream<ChannelReadEvent>? get newMessagesStream => _privateMessagesChannel?.bind('App\\Events\\ChatMessageUpdated');
Stream<ChannelReadEvent>? get newNotificationsStream => _privateNotificationChannel?.bind('Illuminate\\Notifications\\Events\\BroadcastNotificationCreated');
@@ -33,7 +39,7 @@ class WsManager {
Future<void> initialize() async {
if (client == null) {
try {
await TokenStorage.haveTokenInStorage();
if (!await TokenStorage.haveTokenInStorage()) return;
final wsUrl = await wsService.getWebsocket();
final options = PusherChannelsOptions.fromHost(
@@ -71,7 +77,7 @@ class WsManager {
}
}
client?.eventStream.listen((event) async {
_connectionEstablishedSubscription = client?.eventStream.listen((event) async {
if (event.name == 'pusher:connection_established') {
final data = event.data;
final socketId = extractSocketId(data);
@@ -87,21 +93,26 @@ class WsManager {
await _initPrivateNotificationsChannel();
await _initPrivateMessagesChannel();
client?.lifecycleStream.listen((data) {
_lifecycleSubscription = client?.lifecycleStream.listen((data) {
if (data == PusherChannelsClientLifeCycleState.establishedConnection) {
_unsubscribe();
_subscribe();
}
});
client?.eventStream.listen((event) {
EasyDebounce.debounce(
'searchChatRoom',
const Duration(milliseconds: 300),
() => sl<ChatsCubit>().getChatRooms(),
);
_eventSubscription = client?.eventStream.listen((event) {
log('WsManager received event: ${event.name}');
// Только если это не системные сообщения Pusher, обновляем список чатов
if (!event.name.startsWith('pusher:')) {
EasyDebounce.debounce(
'searchChatRoom',
const Duration(milliseconds: 300),
() => sl<ChatsCubit>().getChatRooms(),
);
}
});
_subscribe();
} catch (e, st) {
await Sentry.captureException(e, stackTrace: st);
log(e.toString(), stackTrace: st);
}
}
@@ -166,6 +177,12 @@ class WsManager {
Future<void> clear() async {
await client?.disconnect();
await _lifecycleSubscription?.cancel();
await _eventSubscription?.cancel();
await _connectionEstablishedSubscription?.cancel();
_lifecycleSubscription = null;
_eventSubscription = null;
_connectionEstablishedSubscription = null;
client = null;
userId = null;
_privateDirectChatChannel?.unsubscribe();

View File

@@ -27,7 +27,7 @@ class FeedIconButton extends StatelessWidget {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
svgIcon ?? const SizedBox.shrink(),
?svgIcon,
if (icon != null)
Icon(
icon,