forked from eatvio/eatvio_app
Merge pull request 'feat(notifications)' (#921) from misha/eatvio_app:issue-911 into main
Reviewed-on: eatvio/eatvio_app#921
This commit is contained in:
@@ -9,7 +9,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [ dev-office ]
|
||||
runs-on: [ ubuntu-latest ]
|
||||
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v3
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,6 +21,7 @@ env/.env
|
||||
android/key.properties
|
||||
app/upload-keystore.jks
|
||||
coverage/
|
||||
backend/
|
||||
.fvm/
|
||||
.fvmrc
|
||||
/eatvio
|
||||
@@ -1,6 +1,10 @@
|
||||
package com.eatvioapp.app
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.huawei.hms.push.HmsMessageService
|
||||
import com.huawei.hms.push.RemoteMessage
|
||||
|
||||
@@ -13,6 +17,45 @@ class HmsPushService : HmsMessageService() {
|
||||
override fun onMessageReceived(message: RemoteMessage?) {
|
||||
super.onMessageReceived(message)
|
||||
Log.i("HMS", "onMessageReceived: ${message?.data}")
|
||||
|
||||
if (message == null) return
|
||||
|
||||
// Берём action_url из data-поля, если оно есть.
|
||||
val actionUrl = message.dataOfMap["action_url"]
|
||||
|
||||
// Строим системное уведомление, по тапу по которому откроется MainActivity с action_url.
|
||||
showNotification(message, actionUrl)
|
||||
}
|
||||
|
||||
private fun showNotification(message: RemoteMessage, actionUrl: String?) {
|
||||
val context = applicationContext
|
||||
|
||||
// Интент для запуска Flutter-активити.
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
if (!actionUrl.isNullOrBlank()) {
|
||||
putExtra("action_url", actionUrl)
|
||||
}
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
|
||||
val channelId = "hms_push_channel"
|
||||
|
||||
val notification = NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.mipmap.ic_launcher) // используем иконку приложения
|
||||
.setContentTitle(message.notification?.title ?: context.getString(R.string.app_name))
|
||||
.setContentText(message.notification?.body ?: "")
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
|
||||
NotificationManagerCompat.from(context).notify(message.messageId.hashCode(), notification)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.eatvioapp.app
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
@@ -14,12 +14,13 @@ import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
private val channelName = "com.eatvioapp/hms_push"
|
||||
private var methodChannel: MethodChannel? = null
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
|
||||
.setMethodCallHandler { call, result ->
|
||||
methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
|
||||
methodChannel?.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getHuaweiToken" -> {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
@@ -71,5 +72,33 @@ class MainActivity: FlutterActivity() {
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
// Обрабатываем возможный action_url, с которым была открыта Activity (например, из HMS уведомления).
|
||||
handleIntentForActionUrl(intent)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
// Обработка нового интента, когда Activity уже запущена (тап по уведомлению).
|
||||
handleIntentForActionUrl(intent)
|
||||
}
|
||||
|
||||
private fun handleIntentForActionUrl(intent: Intent?) {
|
||||
if (intent == null) return
|
||||
|
||||
// Вариант 1: action_url лежит в extras.
|
||||
val fromExtras = intent.extras?.getString("action_url")
|
||||
|
||||
// Вариант 2: action_url передан как data Uri.
|
||||
val dataUri = intent.data
|
||||
val fromData = dataUri?.toString()
|
||||
|
||||
val actionUrl = fromExtras ?: fromData
|
||||
if (actionUrl.isNullOrBlank()) return
|
||||
|
||||
android.util.Log.i("HMS", "MainActivity.handleIntentForActionUrl: $actionUrl")
|
||||
|
||||
// Проксируем URL во Flutter, где уже есть общая логика навигации по диплинкам.
|
||||
methodChannel?.invokeMethod("openActionUrl", actionUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import 'package:go_router/go_router.dart';
|
||||
class AppRouter {
|
||||
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
// Домены для deep links
|
||||
static const List<String> _deepLinkHosts = ['eatvio.ru', 'www.eatvio.ru'];
|
||||
// Домены для deep links (в т.ч. ссылки из пушей dev2.eatvio.com)
|
||||
static const List<String> _deepLinkHosts = ['eatvio.ru', 'www.eatvio.ru', 'dev2.eatvio.com', 'www.dev2.eatvio.com'];
|
||||
|
||||
static GoRouter appRouter = GoRouter(
|
||||
initialLocation: RouteConstants.updateScreen,
|
||||
@@ -30,64 +30,59 @@ class AppRouter {
|
||||
],
|
||||
);
|
||||
|
||||
/// Обработка редиректов для deep links
|
||||
/// Возвращает null для обычной навигации, чтобы не мешать внутренним переходам
|
||||
static String? _handleRedirect(BuildContext context, GoRouterState state) {
|
||||
final uri = state.uri;
|
||||
static String? _resolveDeepLinkUri(Uri uri) {
|
||||
final host = uri.host.toLowerCase();
|
||||
final segments = uri.pathSegments;
|
||||
|
||||
// Проверяем, является ли это deep link по домену
|
||||
final isDeepLink = _deepLinkHosts.contains(host);
|
||||
|
||||
// Если это не deep link, не мешаем обычной навигации
|
||||
|
||||
if (!isDeepLink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Логирование для отладки
|
||||
developer.log(
|
||||
'Deep link detected: host=$host, path=${uri.path}, segments=$segments',
|
||||
name: 'AppRouter',
|
||||
);
|
||||
developer.log('Deep link detected: host=$host, path=${uri.path}, segments=$segments', name: 'AppRouter');
|
||||
|
||||
// Проверка на пустой путь или некорректный формат
|
||||
if (segments.isEmpty) {
|
||||
developer.log('Empty segments in deep link, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
|
||||
// Проверка языка (первый сегмент должен быть ru или en)
|
||||
if (segments.length >= 4 &&
|
||||
segments[0] == 'api' &&
|
||||
segments[1] == 'v2' &&
|
||||
segments[2] == 'chat' &&
|
||||
segments[3] == 'show') {
|
||||
developer.log('Deep link: api/v2/chat/show → chat-select-screen', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.chatSelectScreen;
|
||||
}
|
||||
|
||||
if (segments[0] != 'ru' && segments[0] != 'en') {
|
||||
developer.log(
|
||||
'Invalid language segment: ${segments[0]}, redirecting to feed',
|
||||
name: 'AppRouter',
|
||||
);
|
||||
developer.log('Invalid language segment: ${segments[0]}, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
|
||||
// Обработка deep links с учетом языка
|
||||
// Формат: /{lang}/{type}/{identifier?}
|
||||
if (segments.length == 4) {
|
||||
// Формат: /{lang}/diary/{...}
|
||||
final type = segments[1];
|
||||
|
||||
|
||||
if (type == 'diary') {
|
||||
developer.log('Deep link: diary, redirecting to diary-screen', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.diaryScreen;
|
||||
}
|
||||
|
||||
developer.log(
|
||||
'Unknown deep link type for 4 segments: $type, redirecting to feed',
|
||||
name: 'AppRouter',
|
||||
);
|
||||
if (type == 'consultations' && segments[2] == 'status') {
|
||||
final consultationId = segments[3];
|
||||
developer.log('Deep link: consultations/status/$consultationId', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '${RouteConstants.consultationsScreen}?id=$consultationId';
|
||||
}
|
||||
|
||||
developer.log('Unknown deep link type for 4 segments: $type, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
} else if (segments.length == 3) {
|
||||
// Формат: /{lang}/{type}/{identifier}
|
||||
final type = segments[1];
|
||||
final contentIdentifier = segments[2];
|
||||
|
||||
@@ -104,55 +99,111 @@ class AppRouter {
|
||||
developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '/about-plan-screen?slug=$contentIdentifier';
|
||||
case 'diet':
|
||||
developer.log('Deep link: diet/$contentIdentifier', name: 'AppRouter');
|
||||
case 'diet': {
|
||||
final dietId = int.tryParse(contentIdentifier);
|
||||
if (dietId != null) {
|
||||
developer.log('Deep link: diet/id=$dietId', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '/about-diet-screen?id=$dietId';
|
||||
}
|
||||
developer.log('Deep link: diet/slug=$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '/about-diet-screen?slug=$contentIdentifier';
|
||||
}
|
||||
case 'chat':
|
||||
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.chatSelectScreen;
|
||||
case 'profile':
|
||||
// В диплинке передан slug — открываем профиль пользователя по slug.
|
||||
developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '/profile-screen?slug=$contentIdentifier';
|
||||
case 'badge':
|
||||
// В badge нет slug, открываем вкладку «Мой профиль».
|
||||
developer.log('Deep link: badge → my-profile tab', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.myProfileScreen;
|
||||
case 'diary':
|
||||
developer.log('Deep link: diary/$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.diaryScreen;
|
||||
case 'consultant_diary_comment':
|
||||
developer.log('Deep link: consultant_diary_comment/$contentIdentifier → diary', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.diaryScreen;
|
||||
default:
|
||||
developer.log(
|
||||
'Unknown deep link type: $type, redirecting to feed',
|
||||
name: 'AppRouter',
|
||||
);
|
||||
developer.log('Unknown deep link type: $type, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
} else if (segments.length == 2) {
|
||||
// Формат: /{lang}/{type} - только для chat
|
||||
final type = segments[1];
|
||||
|
||||
|
||||
if (type == 'chat') {
|
||||
developer.log('Deep link: chat', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.chatSelectScreen;
|
||||
} else {
|
||||
developer.log(
|
||||
'Unknown deep link type for 2 segments: $type, redirecting to feed',
|
||||
name: 'AppRouter',
|
||||
);
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
} else {
|
||||
// Неизвестный формат deep link
|
||||
developer.log(
|
||||
'Unknown deep link format: ${segments.length} segments, redirecting to feed',
|
||||
name: 'AppRouter',
|
||||
);
|
||||
if (type == 'diary') {
|
||||
developer.log('Deep link: diary', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.diaryScreen;
|
||||
}
|
||||
|
||||
developer.log('Unknown deep link type for 2 segments: $type, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
|
||||
developer.log('Unknown deep link format: ${segments.length} segments, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
|
||||
static String? _handleRedirect(BuildContext context, GoRouterState state) {
|
||||
return _resolveDeepLinkUri(state.uri);
|
||||
}
|
||||
|
||||
static void navigateByDeepLinkUrl(String url) {
|
||||
if (url.isEmpty) {
|
||||
developer.log('navigateByDeepLinkUrl: empty url, redirecting to feed', name: 'AppRouter');
|
||||
appRouter.go(RouteConstants.feedScreen);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri? uri;
|
||||
try {
|
||||
uri = Uri.tryParse(url);
|
||||
} catch (e) {
|
||||
developer.log('navigateByDeepLinkUrl: failed to parse url=$url, error=$e, redirecting to feed', name: 'AppRouter');
|
||||
appRouter.go(RouteConstants.feedScreen);
|
||||
return;
|
||||
}
|
||||
|
||||
if (uri == null) {
|
||||
developer.log('navigateByDeepLinkUrl: uri is null for url=$url, redirecting to feed', name: 'AppRouter');
|
||||
appRouter.go(RouteConstants.feedScreen);
|
||||
return;
|
||||
}
|
||||
|
||||
if (uri.host.isEmpty && uri.pathSegments.isNotEmpty) {
|
||||
uri = Uri(
|
||||
scheme: 'https',
|
||||
host: _deepLinkHosts.first,
|
||||
pathSegments: uri.pathSegments,
|
||||
queryParameters: uri.queryParameters.isEmpty ? null : uri.queryParameters,
|
||||
);
|
||||
}
|
||||
|
||||
final target = _resolveDeepLinkUri(uri);
|
||||
|
||||
if (target != null) {
|
||||
appRouter.go(target);
|
||||
} else {
|
||||
developer.log('navigateByDeepLinkUrl: unresolved url=$url (normalized=${uri.toString()}), redirecting to feed', name: 'AppRouter');
|
||||
appRouter.go(RouteConstants.feedScreen);
|
||||
}
|
||||
}
|
||||
|
||||
static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
|
||||
91
lib/core/notifications/push_navigation_handler.dart
Normal file
91
lib/core/notifications/push_navigation_handler.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Отвечает за навигацию по диплинкам, пришедшим в пуш-уведомлениях.
|
||||
class PushNavigationHandler {
|
||||
static bool _initialized = false;
|
||||
static const MethodChannel _hmsChannel = MethodChannel('com.eatvioapp/hms_push');
|
||||
|
||||
/// Инициализация обработчиков Firebase Messaging.
|
||||
/// Должна вызываться один раз после старта приложения.
|
||||
static Future<void> initialize() async {
|
||||
if (_initialized) return;
|
||||
_initialized = true;
|
||||
|
||||
// Сообщение, по которому запустили приложение из "убитого" состояния.
|
||||
try {
|
||||
final initialMessage = await FirebaseMessaging.instance.getInitialMessage();
|
||||
if (initialMessage != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_handleMessageInternal(initialMessage, source: 'initialMessage');
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
developer.log(
|
||||
'PushNavigationHandler: error while getting initialMessage: $e',
|
||||
name: 'PushNavigationHandler',
|
||||
);
|
||||
}
|
||||
|
||||
// Тап по уведомлению, когда приложение было в фоне.
|
||||
FirebaseMessaging.onMessageOpenedApp.listen(
|
||||
(message) => _handleMessageInternal(message, source: 'onMessageOpenedApp'),
|
||||
);
|
||||
|
||||
// Подписка на события от native-кода (HMS через MethodChannel).
|
||||
_hmsChannel.setMethodCallHandler((call) async {
|
||||
if (call.method == 'openActionUrl') {
|
||||
final url = call.arguments as String? ?? '';
|
||||
handleActionUrlFromNative(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Входная точка для навигации из native-кода (например, HMS через MethodChannel).
|
||||
static void handleActionUrlFromNative(String url) {
|
||||
if (url.isEmpty) {
|
||||
developer.log(
|
||||
'PushNavigationHandler.handleActionUrlFromNative: empty url',
|
||||
name: 'PushNavigationHandler',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
developer.log(
|
||||
'PushNavigationHandler.handleActionUrlFromNative: navigating by url=$url',
|
||||
name: 'PushNavigationHandler',
|
||||
);
|
||||
|
||||
AppRouter.navigateByDeepLinkUrl(url);
|
||||
}
|
||||
|
||||
/// Публичный метод для единичной обработки сообщения (если понадобится где-то ещё).
|
||||
static void handleMessage(RemoteMessage message) {
|
||||
_handleMessageInternal(message, source: 'manualHandle');
|
||||
}
|
||||
|
||||
static void _handleMessageInternal(RemoteMessage message, {required String source}) {
|
||||
final data = message.data;
|
||||
final actionUrl = data['action_url'] as String?;
|
||||
|
||||
if (actionUrl == null || actionUrl.isEmpty) {
|
||||
developer.log(
|
||||
'PushNavigationHandler: no action_url in message data, source=$source, data=$data',
|
||||
name: 'PushNavigationHandler',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
developer.log(
|
||||
'PushNavigationHandler: navigating by action_url=$actionUrl, source=$source',
|
||||
name: 'PushNavigationHandler',
|
||||
);
|
||||
|
||||
AppRouter.navigateByDeepLinkUrl(actionUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,20 @@ class ContentRoutes {
|
||||
name: 'aboutPlanScreen',
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
final commentIdStr = state.uri.queryParameters['commentId'];
|
||||
final commentId = int.tryParse(commentIdStr ?? '');
|
||||
|
||||
if (slug != null) {
|
||||
return AboutPlanScreen(slug: slug, navigateToComments: false, index: null);
|
||||
return AboutPlanScreen(slug: slug, navigateToComments: commentId != null, index: null, highlightedCommentId: commentId);
|
||||
}
|
||||
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
|
||||
return AboutPlanScreen(
|
||||
slug: data['slug'] as String,
|
||||
index: data['index'] as int?,
|
||||
navigateToComments: data['navigateToComments'] as bool? ?? (data['commentId'] != null),
|
||||
highlightedCommentId: data['commentId'] as int?,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -30,12 +38,20 @@ class ContentRoutes {
|
||||
name: 'aboutRecipesScreen',
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
final commentIdStr = state.uri.queryParameters['commentId'];
|
||||
final commentId = int.tryParse(commentIdStr ?? '');
|
||||
|
||||
if (slug != null) {
|
||||
return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null);
|
||||
return AboutRecipeScreen(recipeSlug: slug, navigateToComments: commentId != null, index: null, highlightedCommentId: commentId);
|
||||
}
|
||||
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
|
||||
return AboutRecipeScreen(
|
||||
recipeSlug: data['recipe'] as String,
|
||||
index: data['index'] as int?,
|
||||
navigateToComments: data['navigateToComments'] as bool? ?? (data['commentId'] != null),
|
||||
highlightedCommentId: data['commentId'] as int?,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -43,16 +59,28 @@ class ContentRoutes {
|
||||
name: 'aboutDietScreen',
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
final idStr = state.uri.queryParameters['id'];
|
||||
final id = int.tryParse(idStr ?? '');
|
||||
final commentIdStr = state.uri.queryParameters['commentId'];
|
||||
final commentId = int.tryParse(commentIdStr ?? '');
|
||||
|
||||
if (slug != null) {
|
||||
return AboutDietScreen(id: null, slug: slug, index: null, navigateToComments: false);
|
||||
return AboutDietScreen(id: null, slug: slug, index: null, navigateToComments: commentId != null, highlightedCommentId: commentId);
|
||||
}
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
return AboutDietScreen(
|
||||
id: data['id'] as int?,
|
||||
slug: data['slug'] as String?,
|
||||
index: data['index'] as int?,
|
||||
navigateToComments: data['navigateToComments'] as bool,
|
||||
);
|
||||
if (id != null && id > 0) {
|
||||
return AboutDietScreen(id: id, slug: null, index: null, navigateToComments: commentId != null, highlightedCommentId: commentId);
|
||||
}
|
||||
final data = state.extra as Map<String, dynamic>?;
|
||||
if (data != null) {
|
||||
return AboutDietScreen(
|
||||
id: data['id'] as int?,
|
||||
slug: data['slug'] as String?,
|
||||
index: data['index'] as int?,
|
||||
navigateToComments: data['navigateToComments'] as bool? ?? (data['commentId'] != null),
|
||||
highlightedCommentId: data['commentId'] as int?,
|
||||
);
|
||||
}
|
||||
return const AboutDietScreen(id: null, slug: null, index: null, navigateToComments: false, highlightedCommentId: null);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -65,12 +93,20 @@ class ContentRoutes {
|
||||
name: 'blogPostScreen',
|
||||
builder: (context, state) {
|
||||
final id = state.uri.queryParameters['id'];
|
||||
final commentIdStr = state.uri.queryParameters['commentId'];
|
||||
final commentId = int.tryParse(commentIdStr ?? '');
|
||||
final parsedId = int.tryParse(id ?? '');
|
||||
|
||||
if (parsedId != null) {
|
||||
return BlogPostScreen(id: parsedId);
|
||||
return BlogPostScreen(id: parsedId, highlightedCommentId: commentId);
|
||||
}
|
||||
|
||||
return BlogPostScreen(id: state.extra as int);
|
||||
if (state.extra is int) {
|
||||
return BlogPostScreen(id: state.extra as int);
|
||||
}
|
||||
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
return BlogPostScreen(id: data['id'] as int, highlightedCommentId: data['commentId'] as int?);
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart';
|
||||
@@ -31,8 +32,11 @@ class MainNavigationRoutes {
|
||||
profileAvatarCubit.getMyProfile();
|
||||
});
|
||||
}
|
||||
|
||||
return BottomNavigationBarWidget(child: child);
|
||||
|
||||
return _MainShellWithNavScope(
|
||||
currentLocation: state.matchedLocation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
@@ -58,7 +62,7 @@ class MainNavigationRoutes {
|
||||
GoRoute(
|
||||
path: RouteConstants.diaryScreen,
|
||||
name: 'diaryScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()),
|
||||
pageBuilder: (context, state) => NoTransitionPage(child: DiaryScreen(date: state.extra as DateTime?)),
|
||||
),
|
||||
GoRoute(
|
||||
path: ConsultationsScreen.path,
|
||||
@@ -68,7 +72,10 @@ class MainNavigationRoutes {
|
||||
GoRoute(
|
||||
path: RouteConstants.chatSelectScreen,
|
||||
name: 'chatSelectScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()),
|
||||
pageBuilder: (context, state) {
|
||||
final initialSearchQuery = state.extra is String ? state.extra as String : null;
|
||||
return NoTransitionPage(child: ChatsScreen(initialSearchQuery: initialSearchQuery));
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.notificationsScreen,
|
||||
@@ -94,3 +101,33 @@ class MainNavigationRoutes {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps the shell with [BottomNavScope] so descendants (e.g. notifications)
|
||||
/// can switch tabs via [BottomNavController.switchToRoute] instead of pushing.
|
||||
class _MainShellWithNavScope extends StatefulWidget {
|
||||
const _MainShellWithNavScope({
|
||||
required this.child,
|
||||
required this.currentLocation,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final String currentLocation;
|
||||
|
||||
@override
|
||||
State<_MainShellWithNavScope> createState() => _MainShellWithNavScopeState();
|
||||
}
|
||||
|
||||
class _MainShellWithNavScopeState extends State<_MainShellWithNavScope> {
|
||||
final BottomNavController _navController = BottomNavController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BottomNavScope(
|
||||
controller: _navController,
|
||||
child: BottomNavigationBarWidget(
|
||||
currentLocation: widget.currentLocation,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ class OtherRoutes {
|
||||
GoRoute(
|
||||
path: '${RouteConstants.subsScreen}/:slug',
|
||||
name: 'subsScreen',
|
||||
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType),
|
||||
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug'], subType: state.extra as SubType),
|
||||
),
|
||||
|
||||
// Chat routes
|
||||
@@ -285,8 +285,7 @@ class OtherRoutes {
|
||||
builder: (context, state) {
|
||||
final data = state.extra as Map<String, int>;
|
||||
final diaryId = data['diaryId'] as int;
|
||||
final currentPlanDay = data['currentDiaryDay'] as int;
|
||||
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
|
||||
return UserDiaryScreen(diaryId: diaryId);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
|
||||
@@ -11,6 +11,7 @@ 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';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
|
||||
class WsManager {
|
||||
final WsService wsService;
|
||||
@@ -111,6 +112,7 @@ class WsManager {
|
||||
}
|
||||
});
|
||||
_subscribe();
|
||||
await _connectCubitsToStreams();
|
||||
} catch (e, st) {
|
||||
await Sentry.captureException(e, stackTrace: st);
|
||||
log(e.toString(), stackTrace: st);
|
||||
@@ -118,6 +120,20 @@ class WsManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Подключает ChatsCubit и NotificationsCubit к стримам сокета.
|
||||
/// Вызывается после успешного initialize(), чтобы уведомления и чаты работали
|
||||
/// и при входе уже залогиненного пользователя (редирект на feed), не только после экрана логина.
|
||||
Future<void> _connectCubitsToStreams() async {
|
||||
try {
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
} catch (e, st) {
|
||||
log('WsManager._connectCubitsToStreams: $e', stackTrace: st);
|
||||
await Sentry.captureException(e, stackTrace: st);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initPrivateNotificationsChannel() async {
|
||||
final token = await TokenStorage.getTokenFromStorage();
|
||||
_privateNotificationChannel = client?.privateChannel(
|
||||
|
||||
@@ -21,7 +21,6 @@ import 'package:eatvio_app/core/di/cubit_registrations.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@@ -80,9 +79,6 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
try {
|
||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
showSuccessToast('Успешный вход');
|
||||
} catch (e) {
|
||||
@@ -213,9 +209,6 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
await sl<WsManager>().initialize();
|
||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
showSuccessToast('Успешный вход');
|
||||
} catch (e) {
|
||||
|
||||
@@ -13,7 +13,6 @@ import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/core/utils/validation_utils.dart';
|
||||
@@ -74,9 +73,6 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
try {
|
||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
showSuccessToast('Успешная регистрация');
|
||||
} catch (e) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
||||
@@ -76,9 +75,6 @@ class _ScanQRCodeScreenState extends State<ScanQRCodeScreen> {
|
||||
try {
|
||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
|
||||
@@ -21,8 +21,9 @@ import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class BlogPostScreen extends StatefulWidget {
|
||||
final int id;
|
||||
final int? highlightedCommentId;
|
||||
|
||||
const BlogPostScreen({super.key, required this.id});
|
||||
const BlogPostScreen({super.key, required this.id, this.highlightedCommentId});
|
||||
|
||||
@override
|
||||
State<BlogPostScreen> createState() => _BlogPostScreenState();
|
||||
@@ -303,7 +304,13 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
const SizedBox(height: 16),
|
||||
if (state is CommentsStateLoading) const Loader(),
|
||||
if (state is CommentsStateLoaded)
|
||||
CommentsWidget(id: widget.id.toString(), commentSourceType: CommentSourceType.blogPost, comment: state.comment, replyId: replyId),
|
||||
CommentsWidget(
|
||||
id: widget.id.toString(),
|
||||
commentSourceType: CommentSourceType.blogPost,
|
||||
comment: state.comment,
|
||||
replyId: replyId,
|
||||
highlightedCommentId: widget.highlightedCommentId,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BottomNavController {
|
||||
BottomNavController();
|
||||
|
||||
void Function(String path, Object? extra)? _onSwitchToRoute;
|
||||
|
||||
void register(void Function(String path, Object? extra) callback) {
|
||||
_onSwitchToRoute = callback;
|
||||
}
|
||||
|
||||
void switchToRoute(String path, [Object? extra]) {
|
||||
_onSwitchToRoute?.call(path, extra);
|
||||
}
|
||||
}
|
||||
|
||||
class BottomNavScope extends InheritedWidget {
|
||||
const BottomNavScope({
|
||||
required this.controller,
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final BottomNavController controller;
|
||||
|
||||
static BottomNavController? maybeOf(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<BottomNavScope>()?.controller;
|
||||
}
|
||||
|
||||
static BottomNavController of(BuildContext context) {
|
||||
final controller = maybeOf(context);
|
||||
assert(controller != null, 'No BottomNavScope found in context');
|
||||
return controller!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(BottomNavScope oldWidget) => controller != oldWidget.controller;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import 'package:eatvio_app/features/notifications/presentation/notifications_cub
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
@@ -18,9 +19,15 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class BottomNavigationBarWidget extends StatefulWidget {
|
||||
final Widget child;
|
||||
const BottomNavigationBarWidget({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.currentLocation,
|
||||
});
|
||||
|
||||
const BottomNavigationBarWidget({super.key, required this.child});
|
||||
final Widget child;
|
||||
/// Текущий путь из GoRouter (matchedLocation), чтобы подсветка таба совпадала с экраном при навигации через go() (диплинки, пуши).
|
||||
final String currentLocation;
|
||||
|
||||
@override
|
||||
State<BottomNavigationBarWidget> createState() => _BottomNavigationBarWidgetState();
|
||||
@@ -66,6 +73,24 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Switch to the tab that corresponds to [path], optionally with [extra]
|
||||
/// (e.g. date for diary). If [path] is not in the current tab list, falls back to push.
|
||||
void _switchToRoute(String path, [Object? extra]) {
|
||||
if (pages == null) return;
|
||||
final index = pages!.indexOf(path);
|
||||
if (index >= 0 && index != currentBottomNavBarIndex) {
|
||||
setState(() {
|
||||
currentBottomNavBarIndex = index;
|
||||
});
|
||||
GoRouter.of(context).replace(path, extra: extra);
|
||||
} else if (index >= 0) {
|
||||
// Same tab but with new extra (e.g. diary with date)
|
||||
GoRouter.of(context).replace(path, extra: extra);
|
||||
} else {
|
||||
GoRouter.of(context).push(path, extra: extra);
|
||||
}
|
||||
}
|
||||
|
||||
static const Color _devAccentColor = Color(0xFFF59E0B); // amber-500
|
||||
|
||||
Widget _buildLogoIcon() {
|
||||
@@ -121,6 +146,22 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
// Инициализируем pages на основе profileModel
|
||||
pages ??= _getPages(isConsultant);
|
||||
|
||||
// Expose tab switching to descendants (e.g. notification widget)
|
||||
final controller = BottomNavScope.maybeOf(context);
|
||||
if (controller != null) {
|
||||
controller.register(_switchToRoute);
|
||||
}
|
||||
|
||||
// Подсветка таба по текущему маршруту (совпадает при навигации через go(), в т.ч. диплинки/пуши).
|
||||
final pathWithoutQuery = widget.currentLocation.split('?').first;
|
||||
final indexFromRoute = pages!.indexOf(pathWithoutQuery);
|
||||
final effectiveIndex = indexFromRoute >= 0 ? indexFromRoute : currentBottomNavBarIndex;
|
||||
if (indexFromRoute >= 0 && indexFromRoute != currentBottomNavBarIndex) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) setState(() => currentBottomNavBarIndex = indexFromRoute);
|
||||
});
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: widget.child,
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
@@ -129,7 +170,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
selectedFontSize: 10,
|
||||
unselectedFontSize: 10,
|
||||
selectedItemColor: appTheme.blueA400,
|
||||
currentIndex: currentBottomNavBarIndex,
|
||||
currentIndex: effectiveIndex,
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
label: '',
|
||||
@@ -204,7 +245,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
ImageConstant.icnChat,
|
||||
width: 20,
|
||||
height: 20,
|
||||
colorFilter: pages![currentBottomNavBarIndex] == '/chat-select-screen'
|
||||
colorFilter: pages![effectiveIndex] == '/chat-select-screen'
|
||||
? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)
|
||||
: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
||||
),
|
||||
@@ -226,7 +267,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: SvgPicture.asset(
|
||||
ImageConstant.icnBell,
|
||||
colorFilter: pages![currentBottomNavBarIndex] == '/notifications-screen'
|
||||
colorFilter: pages![effectiveIndex] == '/notifications-screen'
|
||||
? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)
|
||||
: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
||||
width: 20,
|
||||
|
||||
@@ -11,7 +11,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ChatsScreen extends StatefulWidget {
|
||||
const ChatsScreen({super.key});
|
||||
const ChatsScreen({super.key, this.initialSearchQuery});
|
||||
|
||||
/// When set (e.g. from notification tap), the search field is pre-filled
|
||||
/// and chat list is filtered by this query.
|
||||
final String? initialSearchQuery;
|
||||
|
||||
@override
|
||||
State<ChatsScreen> createState() => _ChatsScreenState();
|
||||
@@ -23,12 +27,17 @@ class _ChatsScreenState extends State<ChatsScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Инициализация ChatsCubit при первом открытии экрана
|
||||
final query = widget.initialSearchQuery?.trim();
|
||||
if (query != null && query.isNotEmpty) {
|
||||
searchController.text = query;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final cubit = context.read<ChatsCubit>();
|
||||
final state = cubit.state;
|
||||
if (state.state == BaseState.initial) {
|
||||
cubit.getChatRoomsWithLoading();
|
||||
cubit.getChatRoomsWithLoading(query: query?.isNotEmpty == true ? query : null);
|
||||
} else if (query != null && query.isNotEmpty) {
|
||||
cubit.getChatRooms(query: query);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -172,8 +172,8 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': day.$2.id, 'currentDiaryDay': index + 1});
|
||||
},
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': day.$2.id,});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
@@ -255,7 +255,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
.where((e) => e.$2.state == DiaryStateEnum.finished || e.$2.state == DiaryStateEnum.lastDayFinished)
|
||||
.lastOrNull;
|
||||
if (lastActiveDay != null) {
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': lastActiveDay.$2.id, 'currentDiaryDay': lastActiveDay.$1});
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': lastActiveDay.$2.id,});
|
||||
return;
|
||||
}
|
||||
showErrorDialog(
|
||||
|
||||
@@ -48,6 +48,11 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
|
||||
String comment = '';
|
||||
|
||||
void setInitialDate(DateTime? date) {
|
||||
if (date == null) return;
|
||||
selectedDay = date;
|
||||
}
|
||||
|
||||
Future<bool> setSelectedDay(DateTime day) async {
|
||||
final prevDay = selectedDay;
|
||||
final prevMeal = selectedMeal;
|
||||
@@ -63,7 +68,7 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
return canSelect;
|
||||
}
|
||||
|
||||
Future<bool> fetchDiaryV2({bool needLoading = false, int? diaryId}) async {
|
||||
Future<bool> fetchDiaryV2({bool needLoading = false, int? diaryId, DateTime? date}) async {
|
||||
final currentState = state;
|
||||
try {
|
||||
if (needLoading) {
|
||||
@@ -77,6 +82,7 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
final diaryDay = await getDiary();
|
||||
if (diaryDay == null) {
|
||||
emit(const DiaryState.error(errorMessage: 'Diary not found'));
|
||||
|
||||
@@ -24,31 +24,29 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DiaryScreen extends StatelessWidget {
|
||||
const DiaryScreen({super.key});
|
||||
|
||||
const DiaryScreen({super.key, required this.date});
|
||||
final DateTime? date;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const DiaryScreenContent();
|
||||
return DiaryScreenContent(date: date);
|
||||
}
|
||||
}
|
||||
|
||||
class UserDiaryScreen extends StatelessWidget {
|
||||
static const path = '/user-diary-screen';
|
||||
static const name = 'userDiaryScreen';
|
||||
const UserDiaryScreen({super.key, required this.diaryId, required this.currentPlanDay});
|
||||
const UserDiaryScreen({super.key, required this.diaryId});
|
||||
final int diaryId;
|
||||
final int currentPlanDay;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DiaryScreenContent(diaryId: diaryId, currentPlanDay: currentPlanDay);
|
||||
return DiaryScreenContent(diaryId: diaryId,);
|
||||
}
|
||||
}
|
||||
|
||||
class DiaryScreenContent extends StatefulWidget {
|
||||
const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay});
|
||||
const DiaryScreenContent({super.key, this.diaryId,this.date});
|
||||
final int? diaryId;
|
||||
final int? currentPlanDay;
|
||||
|
||||
final DateTime? date;
|
||||
@override
|
||||
State<DiaryScreenContent> createState() => _DiaryScreenContentState();
|
||||
}
|
||||
@@ -61,7 +59,11 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
|
||||
final s = S.of(context);
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<DiaryCubit>(create: (BuildContext context) => sl<DiaryCubit>()..fetchDiaryV2(needLoading: true, diaryId: widget.diaryId)),
|
||||
BlocProvider<DiaryCubit>(
|
||||
create: (BuildContext context) => sl<DiaryCubit>()
|
||||
..setInitialDate(widget.date)
|
||||
..fetchDiaryV2(needLoading: true, diaryId: widget.diaryId),
|
||||
),
|
||||
BlocProvider(create: (context) => sl<ConsultantCommentsCubit>()),
|
||||
],
|
||||
child: BlocBuilder<DiaryCubit, DiaryState>(
|
||||
@@ -91,7 +93,7 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
if (widget.diaryId == null) const CalendarWidget(),
|
||||
if (widget.diaryId == null) CalendarWidget(date: widget.date),
|
||||
if (state is DiaryStateLoading) ...[const SizedBox(height: 60), const Loader()],
|
||||
if (state is DiaryStateLoaded)
|
||||
WakeupAndRetireWidget(
|
||||
|
||||
@@ -8,7 +8,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
|
||||
class CalendarWidget extends StatefulWidget {
|
||||
const CalendarWidget({super.key});
|
||||
const CalendarWidget({super.key, this.date});
|
||||
final DateTime? date;
|
||||
|
||||
@override
|
||||
State<CalendarWidget> createState() => _CalendarWidgetState();
|
||||
@@ -18,6 +19,23 @@ class _CalendarWidgetState extends State<CalendarWidget> {
|
||||
CalendarFormat _calendarFormat = CalendarFormat.week;
|
||||
DateTime _focusedDay = DateTime.now();
|
||||
DateTime _selectedDay = DateTime.now();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.date != null) {
|
||||
_selectedDay = widget.date!;
|
||||
_focusedDay = widget.date!;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CalendarWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.date != null && widget.date != oldWidget.date) {
|
||||
_selectedDay = widget.date!;
|
||||
_focusedDay = widget.date!;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -37,8 +37,9 @@ class AboutDietScreen extends StatefulWidget {
|
||||
final int? id;
|
||||
final int? index;
|
||||
final bool navigateToComments;
|
||||
final int? highlightedCommentId;
|
||||
|
||||
const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug, required this.id});
|
||||
const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug, required this.id, this.highlightedCommentId});
|
||||
|
||||
@override
|
||||
State<AboutDietScreen> createState() => _AboutDietScreenState();
|
||||
@@ -433,6 +434,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
|
||||
commentSourceType: CommentSourceType.diet,
|
||||
comment: state.comment,
|
||||
replyId: replyId,
|
||||
highlightedCommentId: widget.highlightedCommentId,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -23,14 +23,60 @@ class CommentsWidget extends StatefulWidget {
|
||||
final CommentSourceType commentSourceType;
|
||||
final CommentModel comment;
|
||||
final int? replyId;
|
||||
final int? highlightedCommentId;
|
||||
|
||||
const CommentsWidget({super.key, required this.id, required this.commentSourceType, required this.comment, this.replyId});
|
||||
const CommentsWidget({
|
||||
super.key,
|
||||
required this.id,
|
||||
required this.commentSourceType,
|
||||
required this.comment,
|
||||
this.replyId,
|
||||
this.highlightedCommentId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CommentsWidget> createState() => _CommentsWidgetState();
|
||||
}
|
||||
|
||||
class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
final Map<int, GlobalKey> commentKeys = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.highlightedCommentId != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollToHighlightedComment();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CommentsWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.highlightedCommentId != null && widget.highlightedCommentId != oldWidget.highlightedCommentId) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollToHighlightedComment();
|
||||
});
|
||||
} else if (widget.comment != oldWidget.comment && widget.highlightedCommentId != null) {
|
||||
// Comments might have just loaded
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollToHighlightedComment();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _scrollToHighlightedComment() {
|
||||
final key = commentKeys[widget.highlightedCommentId];
|
||||
if (key != null && key.currentContext != null) {
|
||||
Scrollable.ensureVisible(
|
||||
key.currentContext!,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
@@ -49,8 +95,10 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
}
|
||||
|
||||
Widget _comment(S s, Comment comment, [double radius = 18, bool isChild = false]) {
|
||||
final key = commentKeys.putIfAbsent(comment.id, () => GlobalKey());
|
||||
final isMe = (sl<ProfileAvatarCubit>().state as ProfileAvatarStateLoaded).profile.slug == comment.authorSlug;
|
||||
return GestureDetector(
|
||||
key: key,
|
||||
onTap: () {
|
||||
showCommentsActionsBottomSheet(
|
||||
context,
|
||||
@@ -83,7 +131,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.replyId == comment.id ? appTheme.blueA400.withValues(alpha: 0.1) : Colors.white,
|
||||
color: (widget.replyId == comment.id || widget.highlightedCommentId == comment.id) ? appTheme.blueA400.withValues(alpha: 0.1) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -267,33 +267,113 @@ class NotificationsCubit extends Cubit<NotificationsState> {
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.unknown => UnknownNotification(type: type),
|
||||
NotificationType.consultantEducationUpdated => ConsultantEducationUpdatedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.complaintRegistered => ComplaintRegisteredNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.contentUnpublished => ContentUnpublishedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.contentUnpublishedComplaint => ContentUnpublishedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.financeQuestionCreated => FinanceQuestionCreatedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.newChatMessage => NewChatMessageNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.completeRegistration => CompleteRegistrationNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenDietExpired => ChosenDietExpiredNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.motivateUser => MotivateUserNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.commentComplaint => CommentComplaintNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.commentDeleted => CommentDeletedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.userSubscribed => UserSubscribedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt ?? jsonEncode(notificationJson))),
|
||||
};
|
||||
} catch (e) {
|
||||
return UnknownNotification(type: NotificationType.unknown);
|
||||
return UnknownNotification(type: NotificationType.unknown, createdAt: DateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
void _handleNotification(String notificationData) {
|
||||
final json = jsonDecode(notificationData) as Map<String, dynamic>;
|
||||
|
||||
// Улучшенная логика определения типа уведомления
|
||||
// Улучшенная логика определения типа уведомления (порядок: database_type → type → notification_class_name)
|
||||
String? stringifiedType;
|
||||
|
||||
// Сначала проверяем поле 'type' в корне JSON
|
||||
if (json.containsKey('type') && json['type'] != null) {
|
||||
// Сначала проверяем поле 'database_type' в корне JSON
|
||||
if (json.containsKey('database_type') && json['database_type'] != null) {
|
||||
stringifiedType = json['database_type'].toString();
|
||||
log('Found database_type in root: $stringifiedType');
|
||||
}
|
||||
// Затем поле 'type' в корне
|
||||
else if (json.containsKey('type') && json['type'] != null) {
|
||||
stringifiedType = json['type'].toString();
|
||||
log('Found type in root: $stringifiedType');
|
||||
}
|
||||
// Затем проверяем поле 'notification_class_name' в корне JSON
|
||||
// Затем поле 'notification_class_name' в корне
|
||||
else if (json.containsKey('notification_class_name') && json['notification_class_name'] != null) {
|
||||
stringifiedType = json['notification_class_name'].toString();
|
||||
log('Found notification_class_name in root: $stringifiedType');
|
||||
}
|
||||
// Если есть поле 'data', проверяем внутри него
|
||||
// Если есть поле 'data', проверяем внутри него в том же порядке
|
||||
else if (json.containsKey('data') && json['data'] is Map<String, dynamic>) {
|
||||
final data = json['data'] as Map<String, dynamic>;
|
||||
if (data.containsKey('type') && data['type'] != null) {
|
||||
if (data.containsKey('database_type') && data['database_type'] != null) {
|
||||
stringifiedType = data['database_type'].toString();
|
||||
log('Found database_type in data: $stringifiedType');
|
||||
} else if (data.containsKey('type') && data['type'] != null) {
|
||||
stringifiedType = data['type'].toString();
|
||||
log('Found type in data: $stringifiedType');
|
||||
} else if (data.containsKey('notification_class_name') && data['notification_class_name'] != null) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -413,6 +413,78 @@ class NotificationsService {
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.consultantEducationUpdated => ConsultantEducationUpdatedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.complaintRegistered => ComplaintRegisteredNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.contentUnpublished => ContentUnpublishedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.contentUnpublishedComplaint => ContentUnpublishedNotification.fromJson( // Reusing ContentUnpublishedNotification if structure is same, or create specific if different. Assuming similar for now or check backend.
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.financeQuestionCreated => FinanceQuestionCreatedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.newChatMessage => NewChatMessageNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.completeRegistration => CompleteRegistrationNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenDietExpired => ChosenDietExpiredNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.motivateUser => MotivateUserNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.commentComplaint => CommentComplaintNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.commentDeleted => CommentDeletedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.userSubscribed => UserSubscribedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)),
|
||||
};
|
||||
} catch (e) {
|
||||
|
||||
@@ -32,10 +32,18 @@ import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/widgets/option_widget.dart';
|
||||
|
||||
class AboutPlanScreen extends StatefulWidget {
|
||||
const AboutPlanScreen({super.key, required this.slug, required this.navigateToComments, required this.index});
|
||||
final String slug;
|
||||
final bool navigateToComments;
|
||||
final int? index;
|
||||
final int? highlightedCommentId;
|
||||
|
||||
const AboutPlanScreen({
|
||||
super.key,
|
||||
required this.slug,
|
||||
required this.navigateToComments,
|
||||
required this.index,
|
||||
this.highlightedCommentId,
|
||||
});
|
||||
@override
|
||||
State<AboutPlanScreen> createState() => _AboutPlanScreenState();
|
||||
}
|
||||
@@ -468,13 +476,14 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (commentsState is CommentsStateLoading) const Loader(),
|
||||
if (commentsState is CommentsStateLoaded)
|
||||
CommentsWidget(
|
||||
id: state.planModel?.id.toString() ?? '',
|
||||
commentSourceType: CommentSourceType.plan,
|
||||
comment: commentsState.comment,
|
||||
replyId: replyId,
|
||||
),
|
||||
if (commentsState is CommentsStateLoaded)
|
||||
CommentsWidget(
|
||||
id: state.planModel?.id.toString() ?? '',
|
||||
commentSourceType: CommentSourceType.plan,
|
||||
comment: commentsState.comment,
|
||||
replyId: replyId,
|
||||
highlightedCommentId: widget.highlightedCommentId,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -5,22 +5,30 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
class SubCubit extends Cubit<SubState> {
|
||||
final ProfileRepository profileRepository;
|
||||
|
||||
SubCubit({
|
||||
required this.profileRepository,
|
||||
}) : super(const SubState());
|
||||
SubCubit({required this.profileRepository}) : super(const SubState());
|
||||
|
||||
Future<void> getSubscriptions(String slug, [String? query]) async {
|
||||
Future<void> getSubscriptions(String? slug, [String? query]) async {
|
||||
emit(const SubState.loading());
|
||||
try {
|
||||
if (slug == null) {
|
||||
final myProfile = await profileRepository.getMyProfile();
|
||||
emit(SubState.loaded(subList: await profileRepository.getSubscriptionsBySlug(myProfile.slug, query)));
|
||||
return;
|
||||
}
|
||||
emit(SubState.loaded(subList: await profileRepository.getSubscriptionsBySlug(slug, query)));
|
||||
} catch (e) {
|
||||
emit(SubState.error(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getSubscribers(String slug, [String? query]) async {
|
||||
Future<void> getSubscribers(String? slug, [String? query]) async {
|
||||
emit(const SubState.loading());
|
||||
try {
|
||||
if (slug == null) {
|
||||
final myProfile = await profileRepository.getMyProfile();
|
||||
emit(SubState.loaded(subList: await profileRepository.getSubscribersBySlug(myProfile.slug, query)));
|
||||
return;
|
||||
}
|
||||
emit(SubState.loaded(subList: await profileRepository.getSubscribersBySlug(slug, query)));
|
||||
} catch (e) {
|
||||
emit(SubState.error(errorMessage: e.toString()));
|
||||
|
||||
@@ -12,7 +12,7 @@ enum SubType { subscribers, subscriptions }
|
||||
|
||||
class SubsScreen extends StatefulWidget {
|
||||
final SubType subType;
|
||||
final String slug;
|
||||
final String? slug;
|
||||
|
||||
const SubsScreen({
|
||||
super.key,
|
||||
|
||||
@@ -36,8 +36,9 @@ class AboutRecipeScreen extends StatefulWidget {
|
||||
final String recipeSlug;
|
||||
final int? index;
|
||||
final bool navigateToComments;
|
||||
final int? highlightedCommentId;
|
||||
|
||||
const AboutRecipeScreen({super.key, required this.recipeSlug, required this.index, required this.navigateToComments});
|
||||
const AboutRecipeScreen({super.key, required this.recipeSlug, required this.index, required this.navigateToComments, this.highlightedCommentId});
|
||||
|
||||
@override
|
||||
State<AboutRecipeScreen> createState() => _AboutRecipeScreenState();
|
||||
@@ -391,6 +392,7 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
|
||||
commentSourceType: CommentSourceType.recipe,
|
||||
comment: state.comment,
|
||||
replyId: replyId,
|
||||
highlightedCommentId: widget.highlightedCommentId,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/notifications/push_navigation_handler.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_portal/flutter_portal.dart';
|
||||
@@ -13,9 +14,20 @@ Future<void> main() async {
|
||||
await AppInitializer.runAppWithSentry(() => runApp(const MyApp()));
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
PushNavigationHandler.initialize();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
|
||||
@@ -109,7 +109,10 @@ void main() {
|
||||
],
|
||||
child: wrap(
|
||||
const Scaffold(
|
||||
body: BottomNavigationBarWidget(child: Text('Test Content')),
|
||||
body: BottomNavigationBarWidget(
|
||||
currentLocation: '/feed-screen',
|
||||
child: Text('Test Content'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -145,7 +148,10 @@ void main() {
|
||||
],
|
||||
child: wrap(
|
||||
const Scaffold(
|
||||
body: BottomNavigationBarWidget(child: Text('Test Content')),
|
||||
body: BottomNavigationBarWidget(
|
||||
currentLocation: '/feed-screen',
|
||||
child: Text('Test Content'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -88,7 +88,7 @@ void main() {
|
||||
when(diaryCubit.state).thenReturn(const DiaryState.loading());
|
||||
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty());
|
||||
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true);
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true);
|
||||
|
||||
final commentsCubit = MockConsultantCommentsCubit();
|
||||
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
|
||||
@@ -98,7 +98,7 @@ void main() {
|
||||
sl.registerFactory<DiaryCubit>(() => diaryCubit);
|
||||
sl.registerFactory<ConsultantCommentsCubit>(() => commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(const DiaryScreen()));
|
||||
await tester.pumpWidget(wrap(DiaryScreen(date: DateTime(2025, 1, 1))));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(DiaryScreen), findsOneWidget);
|
||||
@@ -109,7 +109,7 @@ void main() {
|
||||
when(diaryCubit.state).thenReturn(loadedState(hasChosenPlan: true));
|
||||
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty());
|
||||
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true);
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true);
|
||||
|
||||
final commentsCubit = MockConsultantCommentsCubit();
|
||||
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''), dayCommentNeedsToBeFilled: false));
|
||||
@@ -119,7 +119,7 @@ void main() {
|
||||
sl.registerFactory<DiaryCubit>(() => diaryCubit);
|
||||
sl.registerFactory<ConsultantCommentsCubit>(() => commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(const DiaryScreen()));
|
||||
await tester.pumpWidget(wrap(DiaryScreen(date: DateTime(2025, 1, 1))));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 300));
|
||||
|
||||
expect(find.byType(DiaryScreen), findsOneWidget);
|
||||
@@ -132,7 +132,7 @@ void main() {
|
||||
when(diaryCubit.state).thenReturn(const DiaryState.error(errorMessage: 'err'));
|
||||
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty());
|
||||
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true);
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true);
|
||||
|
||||
final commentsCubit = MockConsultantCommentsCubit();
|
||||
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
|
||||
@@ -142,7 +142,7 @@ void main() {
|
||||
sl.registerFactory<DiaryCubit>(() => diaryCubit);
|
||||
sl.registerFactory<ConsultantCommentsCubit>(() => commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(const DiaryScreen()));
|
||||
await tester.pumpWidget(wrap(DiaryScreen(date: DateTime(2025, 1, 1))));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(DiaryScreen), findsOneWidget);
|
||||
@@ -161,7 +161,7 @@ void main() {
|
||||
when(diaryCubit.state).thenReturn(DiaryState.loadedTomorrowWithDiet(diary: futureDiary, selectedDay: DateTime(2025, 1, 2), selectedMeal: 1));
|
||||
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty());
|
||||
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true);
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true);
|
||||
|
||||
final commentsCubit = MockConsultantCommentsCubit();
|
||||
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
|
||||
@@ -171,7 +171,7 @@ void main() {
|
||||
sl.registerFactory<DiaryCubit>(() => diaryCubit);
|
||||
sl.registerFactory<ConsultantCommentsCubit>(() => commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(const DiaryScreen()));
|
||||
await tester.pumpWidget(wrap(DiaryScreen(date: DateTime(2025, 1, 1))));
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
expect(find.byType(DiaryScreen), findsOneWidget);
|
||||
@@ -193,7 +193,7 @@ void main() {
|
||||
when(diaryCubit.state).thenReturn(state);
|
||||
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty());
|
||||
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true);
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true);
|
||||
|
||||
final commentsCubit = MockConsultantCommentsCubit();
|
||||
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: ''), dayCommentNeedsToBeFilled: false));
|
||||
@@ -204,7 +204,7 @@ void main() {
|
||||
sl.registerFactory<DiaryCubit>(() => diaryCubit);
|
||||
sl.registerFactory<ConsultantCommentsCubit>(() => commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(const DiaryScreen()));
|
||||
await tester.pumpWidget(wrap(DiaryScreen(date: DateTime(2025, 1, 1))));
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
expect(find.text('Nice'), findsOneWidget);
|
||||
@@ -217,7 +217,7 @@ void main() {
|
||||
when(diaryCubit.state).thenReturn(state);
|
||||
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty());
|
||||
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'))).thenAnswer((_) async => true);
|
||||
when(diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId'), date: anyNamed('date'))).thenAnswer((_) async => true);
|
||||
|
||||
final commentsCubit = MockConsultantCommentsCubit();
|
||||
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
|
||||
@@ -227,7 +227,7 @@ void main() {
|
||||
sl.registerFactory<DiaryCubit>(() => diaryCubit);
|
||||
sl.registerFactory<ConsultantCommentsCubit>(() => commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(const UserDiaryScreen(diaryId: 1, currentPlanDay: 1)));
|
||||
await tester.pumpWidget(wrap(const UserDiaryScreen(diaryId: 1)));
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
expect(find.byType(AppBar), findsWidgets);
|
||||
|
||||
@@ -101,7 +101,7 @@ void main() {
|
||||
|
||||
registerMocks(cubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false)));
|
||||
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(AboutDietScreen), findsOneWidget);
|
||||
@@ -127,7 +127,7 @@ void main() {
|
||||
registerMocks(cubit, commentsCubit);
|
||||
|
||||
await mockNetworkImagesFor(() async {
|
||||
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false)));
|
||||
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
});
|
||||
@@ -157,7 +157,7 @@ void main() {
|
||||
registerMocks(cubit, commentsCubit);
|
||||
|
||||
await mockNetworkImagesFor(() async {
|
||||
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false)));
|
||||
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
});
|
||||
@@ -183,7 +183,7 @@ void main() {
|
||||
|
||||
registerMocks(cubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false)));
|
||||
await tester.pumpWidget(wrap(cubit, feedCubit, commentsCubit, const AboutDietScreen(slug: 'test', id: null, index: null, navigateToComments: false, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(AboutDietScreen), findsOneWidget);
|
||||
|
||||
@@ -124,7 +124,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('UnknownNotification uses default createdAt when not provided', () {
|
||||
final notification = UnknownNotification(type: NotificationType.unknown);
|
||||
final notification = UnknownNotification(type: NotificationType.unknown, createdAt: DateTime.now());
|
||||
|
||||
expect(notification.type, NotificationType.unknown);
|
||||
expect(notification.isRead, true);
|
||||
@@ -485,8 +485,9 @@ void main() {
|
||||
|
||||
test('PersonalDietCreationNeededNotification.fromJson parses all fields', () {
|
||||
final json = {
|
||||
'plan_name': 'Custom Plan',
|
||||
'client_name': 'John Doe',
|
||||
'user_name': 'John Doe',
|
||||
'consultant_name': 'Consultant',
|
||||
'my_diets_url': 'https://example.com/diets',
|
||||
'created_at': '2024-01-20T12:00:00Z',
|
||||
};
|
||||
|
||||
@@ -496,8 +497,9 @@ void main() {
|
||||
isRead: true,
|
||||
);
|
||||
|
||||
expect(notification.planName, 'Custom Plan');
|
||||
expect(notification.clientName, 'John Doe');
|
||||
expect(notification.userName, 'John Doe');
|
||||
expect(notification.consultantName, 'Consultant');
|
||||
expect(notification.myDietsUrl, 'https://example.com/diets');
|
||||
expect(notification.isRead, true);
|
||||
expect(notification.type, NotificationType.personalDietCreationNeeded);
|
||||
});
|
||||
@@ -510,8 +512,8 @@ void main() {
|
||||
NotificationType.personalDietCreationNeeded,
|
||||
);
|
||||
|
||||
expect(notification.planName, '');
|
||||
expect(notification.clientName, '');
|
||||
expect(notification.userName, '');
|
||||
expect(notification.consultantName, '');
|
||||
});
|
||||
|
||||
test('ChosenPlanClientLastDayFinishedNotification.fromJson parses all fields', () {
|
||||
@@ -551,7 +553,9 @@ void main() {
|
||||
test('StayOnTrackToAuthorNotification.fromJson parses all fields', () {
|
||||
final json = {
|
||||
'plan_name': 'Track Plan',
|
||||
'client_name': 'Jane Smith',
|
||||
'user_name': 'Jane Smith',
|
||||
'day_num': 1,
|
||||
'diary_day_id': 10,
|
||||
'created_at': '2024-01-24T12:00:00Z',
|
||||
};
|
||||
|
||||
@@ -562,7 +566,7 @@ void main() {
|
||||
);
|
||||
|
||||
expect(notification.planName, 'Track Plan');
|
||||
expect(notification.clientName, 'Jane Smith');
|
||||
expect(notification.userName, 'Jane Smith');
|
||||
expect(notification.isRead, false);
|
||||
expect(notification.type, NotificationType.stayOnTrackToAuthor);
|
||||
});
|
||||
|
||||
@@ -50,6 +50,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -89,6 +91,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -99,6 +103,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 2',
|
||||
planId: 2,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -143,6 +149,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -168,6 +176,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -201,6 +211,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -231,6 +243,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -272,6 +286,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -311,6 +327,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -342,6 +360,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -349,6 +369,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 2',
|
||||
planId: 2,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -376,6 +398,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -383,6 +407,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 2',
|
||||
planId: 2,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -426,6 +452,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Plan 1',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -475,6 +503,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -524,6 +554,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -568,6 +600,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -608,6 +642,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -645,6 +681,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -688,6 +726,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -757,6 +797,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -800,6 +842,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -843,6 +887,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -886,6 +932,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -929,6 +977,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -972,6 +1022,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -1015,6 +1067,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -1058,6 +1112,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -1100,6 +1156,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -1137,6 +1195,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -1174,6 +1234,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
@@ -1217,6 +1279,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Initial Plan',
|
||||
planId: 1,
|
||||
consultantName: '',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
|
||||
@@ -36,8 +36,12 @@ void main() {
|
||||
final notification = PlanSubscriptionStartedToAuthorNotification(
|
||||
type: NotificationType.planSubscriptionStartedToAuthor,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
customerName: 'Test Customer',
|
||||
userId: 0,
|
||||
userPhotoPath: '',
|
||||
chatId: 'chat-123',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
@@ -31,8 +31,12 @@ void main() {
|
||||
final notification = PlanSubscriptionStartedToAuthorNotification(
|
||||
type: NotificationType.planSubscriptionStartedToAuthor,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
customerName: 'Test Customer',
|
||||
userId: 0,
|
||||
userPhotoPath: '',
|
||||
chatId: 'chat123',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -47,7 +51,11 @@ void main() {
|
||||
final notification = PlanSubscriptionCanceledToAuthorNotification(
|
||||
type: NotificationType.planSubscriptionCanceledToAuthor,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
customerName: 'Test Customer',
|
||||
userId: 0,
|
||||
userPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -63,6 +71,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClient,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
consultantName: 'Test Consultant',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -77,6 +87,8 @@ void main() {
|
||||
final notification = ChosenPlanTomorrowReminderToClientNotification(
|
||||
type: NotificationType.chosenPlanTomorrowReminderToClient,
|
||||
planName: 'Test Plan',
|
||||
consultantName: '',
|
||||
chatId: 0,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -103,7 +115,12 @@ void main() {
|
||||
testWidgets('displays ConsultantLeftCommentNotification', (tester) async {
|
||||
final notification = ConsultantLeftCommentNotification(
|
||||
type: NotificationType.consultantLeftComment,
|
||||
chosenPlanId: 1,
|
||||
diaryId: 0,
|
||||
date: testDate,
|
||||
planName: '',
|
||||
userName: '',
|
||||
consultantName: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -131,6 +148,11 @@ void main() {
|
||||
final notification = ChosenPlanFinishedToClientNotification(
|
||||
type: NotificationType.chosenPlanFinishedToClient,
|
||||
planName: 'Test Plan',
|
||||
chosenPlanId: 1,
|
||||
consultantName: '',
|
||||
consultantPhotoPath: '',
|
||||
consultantProfileUrl: '',
|
||||
consultantId: 0,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -145,6 +167,11 @@ void main() {
|
||||
final notification = PlanSubscriptionCanceledToCustomerNotification(
|
||||
type: NotificationType.planSubscriptionCanceledToCustomer,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
authorName: '',
|
||||
authorId: 0,
|
||||
authorPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -160,6 +187,10 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToConsultant,
|
||||
planName: 'Test Plan',
|
||||
customerName: 'Test Customer',
|
||||
planId: 1,
|
||||
customerId: 1,
|
||||
peerProfileUrl: '',
|
||||
consultationUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -175,6 +206,7 @@ void main() {
|
||||
type: NotificationType.chosenPlanTomorrowReminderToConsultant,
|
||||
planName: 'Test Plan',
|
||||
customerName: 'Test Customer',
|
||||
chatId: 0,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -189,7 +221,11 @@ void main() {
|
||||
final notification = PlanSubscriptionPaidToAuthorNotification(
|
||||
type: NotificationType.planSubscriptionPaidToAuthor,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
customerName: 'Test Customer',
|
||||
userId: 0,
|
||||
userPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -206,6 +242,9 @@ void main() {
|
||||
planName: 'Test Plan',
|
||||
customerName: 'Test Customer',
|
||||
planId: 1,
|
||||
chosenPlanId: 1,
|
||||
clientPhotoPath: '',
|
||||
clientId: 0,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -219,6 +258,10 @@ void main() {
|
||||
testWidgets('displays ChosenPlanLastDayFinishedForUser', (tester) async {
|
||||
final notification = ChosenPlanLastDayFinishedForUser(
|
||||
type: NotificationType.chosenPlanLastDayFinishedForUser,
|
||||
planName: 'Test Plan',
|
||||
chosenPlanId: 1,
|
||||
complaintUrl: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -252,6 +295,7 @@ void main() {
|
||||
message: 'Test message',
|
||||
entityId: 1,
|
||||
entityType: 'test',
|
||||
entityName: 'Test Entity',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -267,7 +311,7 @@ void main() {
|
||||
type: NotificationType.commentReplied,
|
||||
entityName: 'Test Entity',
|
||||
commentedEntityId: 1,
|
||||
commentedEntityType: 'test',
|
||||
commentedEntityType: CommentedEntityType.diet,
|
||||
author: 'Test Author',
|
||||
authorId: 1,
|
||||
parentCommentId: 1,
|
||||
@@ -305,7 +349,7 @@ void main() {
|
||||
type: NotificationType.entityCommented,
|
||||
entityName: 'Test Entity',
|
||||
entityId: 1,
|
||||
entityType: 'test',
|
||||
entityType: CommentedEntityType.diet,
|
||||
entityTranslatedType: 'Test Type',
|
||||
entityUrl: 'test_url',
|
||||
authorName: 'Test Author',
|
||||
@@ -470,6 +514,7 @@ void main() {
|
||||
final notification = DietCancelledNotification(
|
||||
type: NotificationType.dietCancelled,
|
||||
dietName: 'Test Diet',
|
||||
dietId: 1,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -484,6 +529,7 @@ void main() {
|
||||
final notification = DietStartedNotification(
|
||||
type: NotificationType.dietStarted,
|
||||
dietName: 'Test Diet',
|
||||
dietId: 1,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -498,6 +544,7 @@ void main() {
|
||||
final notification = DietExpiredNotification(
|
||||
type: NotificationType.dietExpired,
|
||||
dietName: 'Test Diet',
|
||||
dietId: 1,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -512,6 +559,7 @@ void main() {
|
||||
final notification = DietWillStartedInNotification(
|
||||
type: NotificationType.dietWillStartedIn,
|
||||
dietName: 'Test Diet',
|
||||
dietId: 1,
|
||||
startDate: '2024-02-01',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
@@ -527,6 +575,7 @@ void main() {
|
||||
final notification = DietCanNotBeStartedNotification(
|
||||
type: NotificationType.dietCanNotBeStarted,
|
||||
dietName: 'Test Diet',
|
||||
dietId: 1,
|
||||
reason: 'Test reason',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
@@ -541,8 +590,9 @@ void main() {
|
||||
testWidgets('displays PersonalDietCreationNeededNotification', (tester) async {
|
||||
final notification = PersonalDietCreationNeededNotification(
|
||||
type: NotificationType.personalDietCreationNeeded,
|
||||
planName: 'Test Plan',
|
||||
clientName: 'Test Client',
|
||||
userName: 'Test Client',
|
||||
consultantName: '',
|
||||
myDietsUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -557,6 +607,9 @@ void main() {
|
||||
final notification = ChosenPlanClientLastDayFinishedNotification(
|
||||
type: NotificationType.chosenPlanClientLastDayFinished,
|
||||
planName: 'Test Plan',
|
||||
chosenPlanId: 1,
|
||||
complaintUrl: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -571,6 +624,8 @@ void main() {
|
||||
final notification = ChosenPlanClientLastDayStartedNotification(
|
||||
type: NotificationType.chosenPlanClientLastDayStarted,
|
||||
planName: 'Test Plan',
|
||||
chosenPlanId: 1,
|
||||
lastDayDiaryUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -585,7 +640,9 @@ void main() {
|
||||
final notification = StayOnTrackToAuthorNotification(
|
||||
type: NotificationType.stayOnTrackToAuthor,
|
||||
planName: 'Test Plan',
|
||||
clientName: 'Test Client',
|
||||
userName: 'Test Client',
|
||||
dayNum: 1,
|
||||
diaryDayId: 0,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -600,6 +657,13 @@ void main() {
|
||||
final notification = ClientStartPlanSubscriptionToSubscriberNotification(
|
||||
type: NotificationType.clientStartPlanSubscriptionToSubscriber,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
authorName: '',
|
||||
authorId: 0,
|
||||
authorPhotoPath: '',
|
||||
chatUrl: '',
|
||||
chatId: '',
|
||||
startDate: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -614,7 +678,11 @@ void main() {
|
||||
final notification = ConsultantConfirmPlanSubscriptionToSubscriberNotification(
|
||||
type: NotificationType.consultantConfirmPlanSubscriptionToSubscriber,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
consultantName: 'Test Consultant',
|
||||
authorId: 0,
|
||||
authorPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -629,8 +697,11 @@ void main() {
|
||||
final notification = ClientPaidPlanSubscriptionToAuthorNotification(
|
||||
type: NotificationType.clientPaidPlanSubscriptionToAuthor,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
clientName: 'Test Client',
|
||||
amount: 100.0,
|
||||
userId: 0,
|
||||
userPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -644,7 +715,10 @@ void main() {
|
||||
testWidgets('displays ConsultantLeftDiaryCommentNotification', (tester) async {
|
||||
final notification = ConsultantLeftDiaryCommentNotification(
|
||||
type: NotificationType.consultantLeftDiaryComment,
|
||||
chosenPlanId: 1,
|
||||
diaryId: 0,
|
||||
planName: 'Test Plan',
|
||||
userName: '',
|
||||
consultantName: 'Test Consultant',
|
||||
diaryDate: testDate,
|
||||
isRead: false,
|
||||
@@ -661,7 +735,11 @@ void main() {
|
||||
final notification = ConsultantConfirmPlanSubscriptionToAuthorNotification(
|
||||
type: NotificationType.consultantConfirmPlanSubscriptionToAuthor,
|
||||
planName: 'Test Plan',
|
||||
consultantName: 'Test Consultant',
|
||||
planId: 1,
|
||||
clientName: 'Test Consultant',
|
||||
userId: 0,
|
||||
userPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -676,7 +754,11 @@ void main() {
|
||||
final notification = ClientPaidPlanSubscriptionToSubscriberNotification(
|
||||
type: NotificationType.clientPaidPlanSubscriptionToSubscriber,
|
||||
planName: 'Test Plan',
|
||||
amount: 100.0,
|
||||
planId: 1,
|
||||
authorName: '',
|
||||
authorId: 0,
|
||||
authorPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -691,7 +773,12 @@ void main() {
|
||||
final notification = ClientStartPlanSubscriptionToAuthorNotification(
|
||||
type: NotificationType.clientStartPlanSubscriptionToAuthor,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
clientName: 'Test Client',
|
||||
userId: 0,
|
||||
userPhotoPath: '',
|
||||
chatId: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -706,7 +793,11 @@ void main() {
|
||||
final notification = ClientCancelPlanSubscriptionToAuthorNotification(
|
||||
type: NotificationType.clientCancelPlanSubscriptionToAuthor,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
clientName: 'Test Client',
|
||||
userId: 0,
|
||||
userPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -721,6 +812,11 @@ void main() {
|
||||
final notification = ClientCancelPlanSubscriptionToSubscriberNotification(
|
||||
type: NotificationType.clientCancelPlanSubscriptionToSubscriber,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
authorName: '',
|
||||
authorId: 0,
|
||||
authorPhotoPath: '',
|
||||
chatUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -734,9 +830,11 @@ void main() {
|
||||
testWidgets('displays UserDailyReportCompletedNotification', (tester) async {
|
||||
final notification = UserDailyReportCompletedNotification(
|
||||
type: NotificationType.userDailyReportCompleted,
|
||||
chosenPlanId: 1,
|
||||
diaryId: 0,
|
||||
planName: 'Test Plan',
|
||||
clientName: 'Test Client',
|
||||
reportDate: testDate,
|
||||
consultantName: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -751,7 +849,8 @@ void main() {
|
||||
final notification = StayOnTrackToCustomerNotification(
|
||||
type: NotificationType.stayOnTrackToCustomer,
|
||||
planName: 'Test Plan',
|
||||
consultantName: 'Test Consultant',
|
||||
dayNum: 1,
|
||||
diaryDayId: 0,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -780,8 +879,9 @@ void main() {
|
||||
testWidgets('displays SubscriptionNotification', (tester) async {
|
||||
final notification = SubscriptionNotification(
|
||||
type: NotificationType.subscription,
|
||||
subscriptionType: 'monthly',
|
||||
planName: 'Test Plan',
|
||||
userName: '',
|
||||
userPhotoPath: '',
|
||||
userProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -795,8 +895,9 @@ void main() {
|
||||
testWidgets('displays BadgeAwardedNotification', (tester) async {
|
||||
final notification = BadgeAwardedNotification(
|
||||
type: NotificationType.badgeAwarded,
|
||||
badgeName: 'Test Badge',
|
||||
badgeDescription: 'Test Description',
|
||||
badgeTitle: 'Test Badge',
|
||||
badgeType: '',
|
||||
badgeImage: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -810,8 +911,9 @@ void main() {
|
||||
testWidgets('displays BadgeRevokedNotification', (tester) async {
|
||||
final notification = BadgeRevokedNotification(
|
||||
type: NotificationType.badgeRevoked,
|
||||
badgeName: 'Test Badge',
|
||||
reason: 'Test reason',
|
||||
badgeTitle: 'Test Badge',
|
||||
badgeType: '',
|
||||
badgeImage: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -839,8 +941,7 @@ void main() {
|
||||
testWidgets('displays ClientCloseComplaintSuccessNotification', (tester) async {
|
||||
final notification = ClientCloseComplaintSuccessNotification(
|
||||
type: NotificationType.clientCloseComplaintSuccess,
|
||||
complaintType: 'test',
|
||||
entityName: 'Test Entity',
|
||||
planName: 'Test Plan',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -887,7 +988,6 @@ void main() {
|
||||
final notification = ClientRejectSuccessNotification(
|
||||
type: NotificationType.clientRejectSuccess,
|
||||
planName: 'Test Plan',
|
||||
reason: 'Test reason',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -903,7 +1003,6 @@ void main() {
|
||||
type: NotificationType.consultantRejectSuccess,
|
||||
planName: 'Test Plan',
|
||||
clientName: 'Test Client',
|
||||
reason: 'Test reason',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -937,8 +1036,12 @@ void main() {
|
||||
final notification = ProductStatusUpdatedForAuthorNotification(
|
||||
type: NotificationType.productStatusUpdatedForAuthor,
|
||||
productName: 'Test Product',
|
||||
oldStatus: 'pending',
|
||||
newStatus: 'approved',
|
||||
productId: 1,
|
||||
author: '',
|
||||
authorId: 0,
|
||||
status: 'approved',
|
||||
subject: '',
|
||||
messageLine: '',
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
);
|
||||
@@ -967,8 +1070,15 @@ void main() {
|
||||
type: NotificationType.personalDietIsDefinedToCustomer,
|
||||
isRead: false,
|
||||
createdAt: testDate,
|
||||
dietName: 'Test Diet',
|
||||
chosenPlanId: 1,
|
||||
dietId: 1,
|
||||
planName: '',
|
||||
userName: '',
|
||||
consultantName: '',
|
||||
dietName: 'Test Diet',
|
||||
role: '',
|
||||
peerSlug: '',
|
||||
peerImagePath: '',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(notification));
|
||||
|
||||
@@ -24,6 +24,8 @@ void main() {
|
||||
type: NotificationType.chosenPlanStartedToClientNotification,
|
||||
planName: 'Test Plan',
|
||||
planId: 1,
|
||||
consultantName: 'Test Consultant',
|
||||
peerProfileUrl: '',
|
||||
isRead: false,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
|
||||
@@ -123,7 +123,7 @@ void main() {
|
||||
|
||||
registerMocks(planCubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null)));
|
||||
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(AboutPlanScreen), findsOneWidget);
|
||||
@@ -149,7 +149,7 @@ void main() {
|
||||
registerMocks(planCubit, commentsCubit);
|
||||
|
||||
await mockNetworkImagesFor(() async {
|
||||
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null)));
|
||||
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
});
|
||||
@@ -177,7 +177,7 @@ void main() {
|
||||
|
||||
registerMocks(planCubit, commentsCubit);
|
||||
|
||||
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null)));
|
||||
await tester.pumpWidget(wrap(planCubit, feedCubit, commentsCubit, const AboutPlanScreen(slug: 'test', navigateToComments: false, index: null, highlightedCommentId: null)));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(AboutPlanScreen), findsOneWidget);
|
||||
|
||||
Reference in New Issue
Block a user