diff --git a/.env b/.env
index e6924f2..0e02e14 100644
--- a/.env
+++ b/.env
@@ -1,4 +1,4 @@
-API_URL=http://node.shoy.publicvm.com/
+API_URL=https://node.shoy.publicvm.com/
giphyApiKey=Ahjpgfo4LVqCACHRcwj0eoMlY5s7u1Uq
-Socket_Url=https://app-dbef67eb-9a2e-44fa-abff-3e8b83204d9c.cleverapps.io
+Socket_Url=https://node.shoy.publicvm.com
serverClientId=1096363232606-2fducjadk56bt4nsreqkj2jna7oiomga.apps.googleusercontent.com
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index ffafe51..1f09463 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,3 @@
{
- "cmake.sourceDirectory": "C:/Users/kerol/Cross_Platform/linux"
+ "cmake.sourceDirectory": "D:/ThirdYear/SWE/project/Cross_Platform/linux"
}
\ No newline at end of file
diff --git a/README.md b/README.md
index a7bc535..9378874 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,34 @@
+## Localization Setup
+
+- Added `flutter_localizations` and `intl` to `pubspec.yaml`.
+- Created `l10n.yaml` to configure Flutter's gen-l10n.
+- Added ARB files in `lib/l10n/`:
+ - `app_en.arb`
+ - `app_ar.arb`
+- Wired delegates and supported locales in `lib/main.dart`.
+
+### How to add strings
+- Edit ARB files under `lib/l10n/` and add new keys.
+- Keep the same keys across languages.
+
+### Generate localization code
+- Run:
+
+ ```powershell
+ flutter gen-l10n
+ ```
+
+- Import and use generated `AppLocalizations` in widgets:
+
+ ```dart
+ import 'package:lite_x/l10n/app_localizations.dart';
+
+ Text(AppLocalizations.of(context)!.trendsTitle)
+ ```
+
+### Switching locales
+- By default, the app supports `en` and `ar` and follows system locale.
+- You can set a specific locale on `MaterialApp.router` by providing `locale: const Locale('ar')`.
# lite_x
Lite X is a Flutter/Riverpod client for the X-like backend that exposes a complete Tweets API surface. The app now exercises every Tweets Interaction endpoint (create/update/delete, likes, retweets, bookmarks, replies, quotes, mentions, summaries, liked tweets, user timelines, and search) through dedicated repositories, view models, and screens.
diff --git a/firebase.json b/firebase.json
index f486d99..b684bcc 100644
--- a/firebase.json
+++ b/firebase.json
@@ -1 +1 @@
-{"flutter":{"platforms":{"android":{"default":{"projectId":"litex-3c6f1","appId":"1:123824690535:android:fc6ea2d45764d44a960bc2","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"litex-3c6f1","configurations":{"android":"1:123824690535:android:fc6ea2d45764d44a960bc2","ios":"1:123824690535:ios:7d4e4fe266a006fe960bc2","macos":"1:123824690535:ios:7d4e4fe266a006fe960bc2","web":"1:123824690535:web:5c24b3d2f16411d1960bc2","windows":"1:123824690535:web:70728cc3cfda8de2960bc2"}}}}}}
\ No newline at end of file
+{"flutter":{"platforms":{"android":{"default":{"projectId":"psychic-fin-474008-h8","appId":"1:112144721859:android:227c69fccfe2ec4c813f76","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"psychic-fin-474008-h8","configurations":{"android":"1:112144721859:android:227c69fccfe2ec4c813f76","ios":"1:112144721859:ios:990931c2c90bbd22813f76","macos":"1:112144721859:ios:990931c2c90bbd22813f76","web":"1:112144721859:web:cd55e199eb8ef04e813f76","windows":"1:112144721859:web:01d0e76c4bc65596813f76"}}}}}}
\ No newline at end of file
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 022c8d2..9d1a9ea 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -1,58 +1,70 @@
-
+
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- Lite X
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- lite_x
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSRequiresIPhoneOS
-
-
- NSPhotoLibraryUsageDescription
- Allow access to your photo library to select and upload images.
- NSPhotoLibraryAddUsageDescription
- Allow the app to save photos to your photo library.
- NSCameraUsageDescription
- Allow access to the camera to take profile photos and capture media.
- NSMicrophoneUsageDescription
- Allow access to the microphone to record audio for media uploads.
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- CADisableMinimumFrameDurationOnPhone
-
- UIApplicationSupportsIndirectInputEvents
-
-
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Lite X
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ lite_x
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ NSPhotoLibraryUsageDescription
+ Pick an image from gallery
+ NSCameraUsageDescription
+ Pick an image from Camera
+ NSMicrophoneUsageDescription
+ enable microphone
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ CADisableMinimumFrameDurationOnPhone
+
+ UIApplicationSupportsIndirectInputEvents
+
+ CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ Editor
+ CFBundleURLName
+ com.Artemsia.lite-x
+ CFBundleURLSchemes
+
+ myapp
+
+
+
+ FlutterDeepLinkingEnabled
+
+
\ No newline at end of file
diff --git a/l10n.yaml b/l10n.yaml
new file mode 100644
index 0000000..28e23bc
--- /dev/null
+++ b/l10n.yaml
@@ -0,0 +1,8 @@
+# Flutter localization configuration
+# See: https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization
+arb-dir: lib/l10n
+template-arb-file: app_en.arb
+output-localization-file: app_localizations.dart
+output-class: AppLocalizations
+nullable-getter: false
+untranslated-messages-file: build/untranslated_messages.txt
diff --git a/lib/core/classes/PickedImage.dart b/lib/core/classes/PickedImage.dart
index 49c98a2..3f2ba6a 100644
--- a/lib/core/classes/PickedImage.dart
+++ b/lib/core/classes/PickedImage.dart
@@ -42,3 +42,21 @@ Future> pickImages({int maxImages = 4}) async {
return [];
}
}
+
+Future pickVideo() async {
+ try {
+ final ImagePicker picker = ImagePicker();
+ final XFile? video = await picker.pickVideo(source: ImageSource.gallery);
+
+ if (video != null) {
+ return PickedImage(
+ file: File(video.path),
+ name: video.name,
+ path: video.path,
+ );
+ }
+ return null;
+ } catch (e) {
+ return null;
+ }
+}
diff --git a/lib/core/models/usermodel.dart b/lib/core/models/usermodel.dart
index 555fb80..7c7993e 100644
--- a/lib/core/models/usermodel.dart
+++ b/lib/core/models/usermodel.dart
@@ -20,7 +20,7 @@ class UserModel {
final String username;
@HiveField(4)
- final String? photo;
+ final String? photo; // media Id
@HiveField(5)
final String? bio;
@@ -40,7 +40,7 @@ class UserModel {
@HiveField(10)
final Set interests;
@HiveField(11)
- final String? localProfilePhotoPath;
+ final String? localProfilePhotoPath; // path of local profile photo
UserModel({
required this.name,
diff --git a/lib/core/providers/unseenChatsCountProvider.dart b/lib/core/providers/unseenChatsCountProvider.dart
new file mode 100644
index 0000000..31f764f
--- /dev/null
+++ b/lib/core/providers/unseenChatsCountProvider.dart
@@ -0,0 +1,3 @@
+import 'package:flutter_riverpod/legacy.dart';
+
+final unseenChatsCountProvider = StateProvider((ref) => 0);
diff --git a/lib/core/routes/AppRouter.dart b/lib/core/routes/AppRouter.dart
index d43296a..e0e63d4 100644
--- a/lib/core/routes/AppRouter.dart
+++ b/lib/core/routes/AppRouter.dart
@@ -19,11 +19,12 @@ import 'package:lite_x/features/auth/view/screens/Create_Account/Password_Screen
import 'package:lite_x/features/auth/view/screens/Create_Account/Upload_Profile_Photo_Screen.dart';
import 'package:lite_x/features/auth/view/screens/Create_Account/Verification_Screen.dart';
import 'package:lite_x/features/auth/view/screens/Log_In/VerificationForgot_Screen.dart';
-import 'package:lite_x/features/chat/view/screens/Search_Direct_messages.dart';
+import 'package:lite_x/features/auth/view/screens/Oauth/SetBirthdate.dart';
import 'package:lite_x/features/chat/view/screens/Search_User_Group.dart';
import 'package:lite_x/features/chat/view/screens/chat_Screen.dart';
import 'package:lite_x/features/chat/view/screens/conversations_screen.dart';
import 'package:lite_x/features/explore/view/explore_screen.dart';
+import 'package:lite_x/features/trends/view/screens/trends_screen.dart';
import 'package:lite_x/features/home/view/screens/tweet_screen.dart';
import 'package:lite_x/features/profile/models/profile_model.dart';
import 'package:lite_x/features/profile/models/shared.dart';
@@ -51,36 +52,12 @@ import 'package:lite_x/features/settings/screens/UserName_Screen.dart';
import 'package:lite_x/features/settings/screens/YourAccount_Screen.dart';
import 'package:lite_x/features/settings/screens/AccountInformation_Screen.dart';
import 'package:lite_x/features/settings/screens/ChangePassword_Screen.dart';
+import 'package:lite_x/features/notifications/view/screens/Notification_Screen.dart';
class Approuter {
static final GoRouter router = GoRouter(
- // initialLocation: "/appshell",
initialLocation: "/splash",
- // initialExtra: ProfileModel(
- // id: "",
- // username: "hazememam",
- // displayName: "Hazem Emam",
- // email: "hazem@gmail.com",
- // bio: "Hello from hazem emam ",
- // avatarUrl:
- // "https://images.pexels.com/photos/31510092/pexels-photo-31510092.jpeg",
- // bannerUrl:
- // "https://images.pexels.com/photos/1765033/pexels-photo-1765033.jpeg",
- // followersCount: 15,
- // followingCount: 20,
- // tweetsCount: 15,
- // isVerified: false,
- // joinedDate: formatDate(DateTime(2004, 8, 21), DateFormatType.fullDate),
- // website: "https://google.cof",
- // location: "cairo",
- // postCount: 2,
- // birthDate: formatDate(DateTime(2004, 8, 21), DateFormatType.fullDate),
- // isFollowing: false,
- // isFollower: false,
- // protectedAccount: false,
- // isBlockedByMe: true,
- // isMutedByMe: false,
- // ),
+ // initialLocation: "/trends",
routes: [
GoRoute(
name: RouteConstants.splash,
@@ -184,6 +161,14 @@ class Approuter {
transitionsBuilder: _slideRightTransitionBuilder,
),
),
+ GoRoute(
+ name: RouteConstants.setbirthdate,
+ path: "/setbirthdate",
+ pageBuilder: (context, state) => CustomTransitionPage(
+ child: const Setbirthdate(),
+ transitionsBuilder: _slideRightTransitionBuilder,
+ ),
+ ),
GoRoute(
name: RouteConstants.ForgotpasswordScreen,
path: "/ForgotpasswordScreen",
@@ -279,14 +264,6 @@ class Approuter {
),
),
- GoRoute(
- name: RouteConstants.SearchDirectMessages,
- path: "/SearchDirectMessages",
- pageBuilder: (context, state) => CustomTransitionPage(
- child: const SearchDirectMessages(),
- transitionsBuilder: _slideRightTransitionBuilder,
- ),
- ),
GoRoute(
name: RouteConstants.Interests,
path: "/Interests",
@@ -366,6 +343,8 @@ class Approuter {
subtitle: extraData['subtitle'],
profileImage: extraData['avatarUrl'],
isGroup: extraData['isGroup'] ?? false,
+ recipientFollowersCount:
+ extraData['recipientFollowersCount'] ?? 0,
),
transitionsBuilder: _slideRightTransitionBuilder,
);
@@ -385,7 +364,7 @@ class Approuter {
name: RouteConstants.SearchScreen,
path: "/searchScreen",
pageBuilder: (context, state) => CustomTransitionPage(
- child: SearchScreen(),
+ child: SearchScreen(extra: state.extra as Map?),
transitionsBuilder: _slideRightTransitionBuilder,
),
),
@@ -397,6 +376,14 @@ class Approuter {
transitionsBuilder: _slideRightTransitionBuilder,
),
),
+ GoRoute(
+ name: RouteConstants.TrendsScreen,
+ path: "/trends",
+ pageBuilder: (context, state) => CustomTransitionPage(
+ child: const TrendsScreen(),
+ transitionsBuilder: _slideRightTransitionBuilder,
+ ),
+ ),
GoRoute(
name: RouteConstants.ProfilePhotoScreen,
path: "/profilePhotoScreen",
@@ -453,6 +440,14 @@ class Approuter {
transitionsBuilder: _slideRightTransitionBuilder,
),
),
+ GoRoute(
+ name: RouteConstants.notifications,
+ path: "/notifications",
+ pageBuilder: (context, state) => CustomTransitionPage(
+ child: const NotificationScreen(),
+ transitionsBuilder: _slideRightTransitionBuilder,
+ ),
+ ),
],
redirect: (context, state) {
return null;
diff --git a/lib/core/routes/Route_Constants.dart b/lib/core/routes/Route_Constants.dart
index 7dc85b6..da1c993 100644
--- a/lib/core/routes/Route_Constants.dart
+++ b/lib/core/routes/Route_Constants.dart
@@ -19,6 +19,7 @@ class RouteConstants {
static String changePasswordScreen = "changePasswordScreen";
static String Interests = "Interests";
static String usernamesettings = "usernamesettings";
+ static String setbirthdate = "setbirthdate";
static String FollowingFollowersScreen = "FollowingFollowersScreen";
static String BirthDateScreen = "BirthDateScreen";
@@ -45,11 +46,17 @@ class RouteConstants {
"VerifyChangeEmailProfileScreen";
// search feature
- static String SearchScreen = "SearchScreen";
+ static String SearchScreen = "searchScreen";
// explore feature
static String ExploreScreen = "ExploreScreen";
static String ExploreProfileScreen = "ExploreProfileScreen";
+
+ // trends feature
+ static String TrendsScreen = "TrendsScreen";
+
+
static String TweetDetailsScreen = "TweetDetailsScreen";
+ static String notifications = "notifications";
}
diff --git a/lib/core/services/deep_link_service.dart b/lib/core/services/deep_link_service.dart
index 1ec4d1a..59ab16b 100644
--- a/lib/core/services/deep_link_service.dart
+++ b/lib/core/services/deep_link_service.dart
@@ -21,7 +21,6 @@ class DeepLinkService {
}
});
- // إضافة مراقب لحالة التطبيق
WidgetsBinding.instance.addObserver(_observer);
}
diff --git a/lib/core/utils.dart b/lib/core/utils.dart
index e3850cf..97525a2 100644
--- a/lib/core/utils.dart
+++ b/lib/core/utils.dart
@@ -72,31 +72,31 @@ String? passwordValidator(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
- if (value.length < 8) {
- return 'Password must be at least 8 characters';
+ if (value.length < 9) {
+ return 'Password must be at least 9 characters';
}
if (value.length > 256) {
return 'Password must be less than 256 characters';
}
final capitalLetters = RegExp(r'[A-Z]');
- if (capitalLetters.allMatches(value).length < 3) {
- return 'Password must contain at least 3 uppercase letters';
+ if (capitalLetters.allMatches(value).length < 1) {
+ return 'Password must contain at least 1 uppercase letter';
}
final lowercaseLetters = RegExp(r'[a-z]');
- if (lowercaseLetters.allMatches(value).length < 3) {
- return 'Password must contain at least 3 lowercase letters';
+ if (lowercaseLetters.allMatches(value).length < 1) {
+ return 'Password must contain at least 1 lowercase letter';
}
final symbols = RegExp(r'[!@#\$%^&*(),.?":{}|<>_\-=+;]');
- if (symbols.allMatches(value).length < 3) {
- return 'Password must contain at least 3 symbols';
+ if (symbols.allMatches(value).length < 1) {
+ return 'Password must contain at least 1 symbol';
}
final numbers = RegExp(r'\d');
- if (numbers.allMatches(value).length < 3) {
- return 'Password must contain at least 3 numbers';
+ if (numbers.allMatches(value).length < 1) {
+ return 'Password must contain at least 1 number';
}
return null;
diff --git a/lib/core/view/screen/app_shell.dart b/lib/core/view/screen/app_shell.dart
index 86128f0..f7107c8 100644
--- a/lib/core/view/screen/app_shell.dart
+++ b/lib/core/view/screen/app_shell.dart
@@ -6,6 +6,7 @@ import 'package:lite_x/features/chat/view/screens/conversations_screen.dart';
import 'package:lite_x/features/home/view/screens/home_screen.dart';
import 'package:lite_x/features/profile/view/screens/explore_profile_screen.dart';
import 'package:lite_x/features/shared/widgets/bottom_navigation.dart';
+import 'package:lite_x/features/notifications/view/screens/Notification_Screen.dart';
// Provider for managing which tab is selected
final shellNavigationProvider = StateProvider((ref) => 0);
@@ -30,24 +31,9 @@ class AppShell extends ConsumerWidget {
// _buildSearchScreen(), // Index 1 - Search
ExploreProfileScreen(),
_buildCommunitiesScreen(), // Index 2 - Communities
- _buildNotificationsScreen(), // Index 3 - Notifications
+ NotificationScreen(), // Index 3 - Notifications
ConversationsScreen(),
- //// Index 4 - Messages
- // ListView(
- // children: [
- // Padding(
- // padding: const EdgeInsets.all(24),
- // child: Text(
- // "Nothing to see here -- yet.",
- // style: TextStyle(
- // color: Colors.white,
- // fontWeight: FontWeight.bold,
- // fontSize: 35,
- // ),
- // ),
- // ),
- // ],
- // ),
+
],
),
bottomNavigationBar: AnimatedContainer(
@@ -65,16 +51,6 @@ class AppShell extends ConsumerWidget {
);
}
- // Placeholder screens - you'll create these later
- Widget _buildSearchScreen() {
- return const Center(
- child: Text(
- 'Search Screen',
- style: TextStyle(color: Colors.white, fontSize: 24),
- ),
- );
- }
-
Widget _buildNotificationsScreen() {
return const Center(
child: Text(
diff --git a/lib/features/auth/repositories/auth_remote_repository.dart b/lib/features/auth/repositories/auth_remote_repository.dart
index 263e914..8474d8c 100644
--- a/lib/features/auth/repositories/auth_remote_repository.dart
+++ b/lib/features/auth/repositories/auth_remote_repository.dart
@@ -1,4 +1,3 @@
-// ignore_for_file: unused_catch_clause
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
@@ -32,10 +31,9 @@ class AuthRemoteRepository {
try {
final baseUrl = dotenv.env["API_URL"]!;
final authUrl = "${baseUrl}oauth2/authorize/github";
- final fullUrl = "$authUrl?redirect_uri=${baseUrl}login/success";
final opened = await launchUrl(
- Uri.parse(fullUrl),
+ Uri.parse(authUrl),
mode: LaunchMode.externalApplication,
);
@@ -50,7 +48,7 @@ class AuthRemoteRepository {
}
final token = uri.queryParameters["token"];
- final refresh = uri.queryParameters["refresh"];
+ final refresh = uri.queryParameters["refresh-token"];
final userRaw = uri.queryParameters["user"];
if (token == null || refresh == null || userRaw == null) {
@@ -76,7 +74,7 @@ class AuthRemoteRepository {
final _googleSignIn = signIn.GoogleSignIn(
serverClientId:
- "1096363232606-2fducjadk56bt4nsreqkj2jna7oiomga.apps.googleusercontent.com",
+ "https://1096363232606-2fducjadk56bt4nsreqkj2jna7oiomga.apps.googleusercontent.com",
scopes: ['email', 'https://www.googleapis.com/auth/userinfo.profile'],
);
@@ -134,7 +132,7 @@ class AuthRemoteRepository {
data: {'name': name, 'email': email, 'dateOfBirth': dateOfBirth},
);
return right(response.data['message'] ?? 'Verification email sent');
- } on DioException catch (e) {
+ } on DioException {
return left(AppFailure(message: 'Signup failed'));
} catch (e) {
return left(AppFailure(message: e.toString()));
@@ -154,7 +152,7 @@ class AuthRemoteRepository {
final message = response.data['message'] ?? 'Verified successfully';
return right(message);
- } on DioException catch (e) {
+ } on DioException {
return left(AppFailure(message: 'Email verification failed'));
} catch (e) {
return left(AppFailure(message: e.toString()));
@@ -171,11 +169,12 @@ class AuthRemoteRepository {
'api/auth/finalize_signup',
data: {'email': email, 'password': password},
);
-
+ print("asermohamed${response.data['tokens']}");
final user = UserModel.fromMap(response.data['user']);
final tokens = TokensModel.fromMap(response.data['tokens']);
+
return right((user, tokens));
- } on DioException catch (e) {
+ } on DioException {
return left(AppFailure(message: 'Signup failed'));
} catch (e) {
return left(AppFailure(message: e.toString()));
@@ -235,7 +234,7 @@ class AuthRemoteRepository {
print("MEDIA ID AFTER UPLOAD: $mediaId");
return right({'mediaId': mediaId, 'keyName': newMediaKey});
- } on DioException catch (e) {
+ } on DioException {
return left(AppFailure(message: 'Upload failed'));
} catch (e) {
return left(AppFailure(message: e.toString()));
@@ -269,6 +268,19 @@ class AuthRemoteRepository {
}
}
+ //-----------------------------------------------------------------------updateprofilephoto----------------------------------------------------------------------------------//
+ Future> updateProfilePhoto(
+ String userId,
+ String mediaId,
+ ) async {
+ try {
+ await _dio.patch("api/users/profile-picture/$userId/$mediaId");
+ return const Right(());
+ } catch (e) {
+ return Left(AppFailure(message: "couldn't update profile picture"));
+ }
+ }
+
//-------------------------------------------------------------------------------------------------------------------------------------------------------//
Future> updateUsername({
required UserModel currentUser,
@@ -283,14 +295,32 @@ class AuthRemoteRepository {
final updatedUser = currentUser.copyWith(username: newUsername);
final newtokens = TokensModel.fromMap_update(response.data['tokens']);
return right((updatedUser, newtokens));
- } on DioException catch (e) {
+ } on DioException {
return left(AppFailure(message: 'Failed to update username'));
} catch (e) {
return left(AppFailure(message: e.toString()));
}
}
- //---------------------------------------------------------google sign up--------------------------------------------------------------------------//
+ //---------------------------------------------------------setbirthdate-------------------------------------------------------------------------//
+ Future> setbirthdate({
+ required String day,
+ required String month,
+ required String year,
+ }) async {
+ try {
+ final response = await _dio.post(
+ 'api/auth/set-birthdate',
+ data: {'day': day, 'month': month, 'year': year},
+ );
+ final message = response.data['message'] as String;
+ return right(message);
+ } on DioException {
+ return left(AppFailure(message: 'Failed to set birthdate'));
+ } catch (e) {
+ return left(AppFailure(message: e.toString()));
+ }
+ }
//-------------------------------------------------FCM Token Registration-----------------------------------------------------------------------------------------//
Future> registerFcmToken({
@@ -339,15 +369,17 @@ class AuthRemoteRepository {
final user = UserModel.fromMap(response.data['user']);
final tokens = TokensModel.fromMap_login(response.data);
- // print(tokens);
+ print("asermohamed${user.id}");
+ print("asermohamed${tokens.accessToken}");
return right((user, tokens));
- } on DioException catch (e) {
+ } on DioException {
return left(AppFailure(message: 'Login failed'));
} catch (e) {
return left(AppFailure(message: "Wrong Password"));
}
}
+ //-----------------------------------------------check email-------------------------------------------------------------------------------------//
Future> check_email({required String email}) async {
try {
final response = await _dio.post(
@@ -355,13 +387,31 @@ class AuthRemoteRepository {
data: {'email': email},
);
return right(response.data['exists'] ?? false);
- } on DioException catch (e) {
+ } on DioException {
return left(AppFailure(message: 'Email check failed'));
} catch (e) {
return left(AppFailure(message: e.toString()));
}
}
+ //--------------------------------------suggest usernames--------------------------------------//
+ Future>> suggest_usernames({
+ required String username,
+ }) async {
+ try {
+ final response = await _dio.post(
+ 'api/auth/suggest-usernames',
+ data: {'name': username},
+ );
+ final suggestions = List.from(response.data['suggestions'] ?? []);
+ return right(suggestions);
+ } on DioException {
+ return left(AppFailure(message: 'Username suggestions failed'));
+ } catch (e) {
+ return left(AppFailure(message: e.toString()));
+ }
+ }
+
//--------------------------------------forgetpassword---------------------------------------------------------------//
Future> forget_password({
required String email,
@@ -373,7 +423,7 @@ class AuthRemoteRepository {
);
final message = response.data['message'] ?? 'Reset code sent';
return right(message);
- } on DioException catch (e) {
+ } on DioException {
return left(AppFailure(message: 'Forget password failed'));
} catch (e) {
return left(AppFailure(message: e.toString()));
@@ -391,7 +441,7 @@ class AuthRemoteRepository {
);
final message = response.data['message'] ?? 'Reset code verified';
return right(message);
- } on DioException catch (e) {
+ } on DioException {
return left(AppFailure(message: 'Verify reset code failed'));
} catch (e) {
return left(AppFailure(message: e.toString()));
@@ -410,7 +460,7 @@ class AuthRemoteRepository {
final user = UserModel.fromMap(response.data['user']);
final tokens = TokensModel.fromMap_reset_password(response.data);
return right((user, tokens));
- } on DioException catch (e) {
+ } on DioException {
return left(AppFailure(message: 'Reset password failed'));
} catch (e) {
return left(AppFailure(message: e.toString()));
@@ -437,7 +487,7 @@ class AuthRemoteRepository {
final message =
response.data['message'] ?? 'Password updated successfully';
return right(message);
- } on DioException catch (e) {
+ } on DioException {
return left(AppFailure(message: 'update password failed'));
} catch (e) {
return left(AppFailure(message: e.toString()));
@@ -455,7 +505,7 @@ class AuthRemoteRepository {
);
final message = response.data['message'] ?? 'Email updated successfully';
return right(message);
- } on DioException catch (e) {
+ } on DioException {
return left(AppFailure(message: 'update email failed'));
} catch (e) {
return left(AppFailure(message: e.toString()));
@@ -474,7 +524,7 @@ class AuthRemoteRepository {
final message = response.data['message'] ?? 'updated email successfully';
return right(message);
- } on DioException catch (e) {
+ } on DioException {
return left(AppFailure(message: 'Email update failed'));
} catch (e) {
return left(AppFailure(message: e.toString()));
diff --git a/lib/features/auth/view/screens/Create_Account/CreateAccount_Screen.dart b/lib/features/auth/view/screens/Create_Account/CreateAccount_Screen.dart
index ef94e27..329eeed 100644
--- a/lib/features/auth/view/screens/Create_Account/CreateAccount_Screen.dart
+++ b/lib/features/auth/view/screens/Create_Account/CreateAccount_Screen.dart
@@ -267,7 +267,7 @@ class _CreateAccountScreenState extends ConsumerState {
color: Palette.textWhite,
),
),
- const SizedBox(height: 150),
+ const SizedBox(height: 100),
CustomTextField(
controller: _nameController,
labelText: 'Name',
@@ -279,7 +279,7 @@ class _CreateAccountScreenState extends ConsumerState {
context,
).requestFocus(_emailFocus),
),
- const SizedBox(height: 10),
+ const SizedBox(height: 15),
CustomTextField(
controller: _emailController,
labelText: 'Email',
@@ -289,7 +289,7 @@ class _CreateAccountScreenState extends ConsumerState {
focusNode: _emailFocus,
validationState: _emailState,
),
- const SizedBox(height: 25),
+ const SizedBox(height: 15),
CustomTextField(
controller: _dobController,
labelText: 'Date of birth',
@@ -303,7 +303,7 @@ class _CreateAccountScreenState extends ConsumerState {
),
),
_buildNextButton(isLoading),
- const SizedBox(height: 15),
+ const SizedBox(height: 5),
],
),
),
@@ -322,7 +322,6 @@ class _CreateAccountScreenState extends ConsumerState {
padding: const EdgeInsets.all(10),
alignment: Alignment.centerRight,
child: SizedBox(
- width: 90,
child: ElevatedButton(
onPressed: (_isFormValid && !isLoading) ? _handleNext : null,
style: ElevatedButton.styleFrom(
@@ -330,7 +329,7 @@ class _CreateAccountScreenState extends ConsumerState {
disabledBackgroundColor: Palette.textWhite.withOpacity(0.5),
foregroundColor: Palette.background,
disabledForegroundColor: Palette.border,
- minimumSize: const Size(0, 40),
+ minimumSize: const Size(0, 50),
),
child: const Text(
'Next',
diff --git a/lib/features/auth/view/screens/Create_Account/Password_Screen.dart b/lib/features/auth/view/screens/Create_Account/Password_Screen.dart
index ed53692..0c785d9 100644
--- a/lib/features/auth/view/screens/Create_Account/Password_Screen.dart
+++ b/lib/features/auth/view/screens/Create_Account/Password_Screen.dart
@@ -177,7 +177,7 @@ class _PasswordScreenState extends ConsumerState {
valueListenable: _isFormValid,
builder: (context, isValid, child) {
return SizedBox(
- width: 120,
+ // width: 120,
child: ElevatedButton(
onPressed: (isValid && !isLoading) ? _handleSignUp : null,
style: ElevatedButton.styleFrom(
@@ -185,7 +185,7 @@ class _PasswordScreenState extends ConsumerState {
disabledBackgroundColor: Palette.textWhite.withOpacity(0.5),
foregroundColor: Palette.background,
disabledForegroundColor: Palette.border,
- minimumSize: const Size(0, 38),
+ minimumSize: const Size(0, 50),
),
child: isLoading
? const SizedBox(
diff --git a/lib/features/auth/view/screens/Create_Account/UserName_Screen.dart b/lib/features/auth/view/screens/Create_Account/UserName_Screen.dart
index 354c7ca..eb091d7 100644
--- a/lib/features/auth/view/screens/Create_Account/UserName_Screen.dart
+++ b/lib/features/auth/view/screens/Create_Account/UserName_Screen.dart
@@ -1,4 +1,4 @@
-import 'dart:math';
+import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
@@ -24,6 +24,8 @@ class _UsernameScreenState extends ConsumerState {
final _usernameController = TextEditingController();
final _isFormValid = ValueNotifier(false);
List suggestions = [];
+ Timer? _debounce;
+ bool _isSuggestionLoading = false;
@override
void initState() {
@@ -33,20 +35,43 @@ class _UsernameScreenState extends ConsumerState {
}
void _validateForm() {
- _isFormValid.value =
+ final isValid =
_usernameController.text.trim().isNotEmpty &&
- _usernameController.text.trim().length >= 3;
+ _usernameController.text.trim().length > 3;
+
+ _isFormValid.value = isValid;
+
+ if (_usernameController.text.trim().length > 3) {
+ _fetchSuggestions(_usernameController.text.trim());
+ } else {
+ if (suggestions.isNotEmpty || _isSuggestionLoading) {
+ setState(() {
+ suggestions = [];
+ _isSuggestionLoading = false;
+ });
+ }
+ }
}
- void generateSuggestions(String name) {
- final clean = name.replaceAll(" ", "").toLowerCase();
+ void _fetchSuggestions(String name) {
+ if (_debounce?.isActive ?? false) _debounce!.cancel();
+
setState(() {
- suggestions = [
- "${clean}${Random().nextInt(50)}",
- "${clean}_x",
- "${clean}_${DateTime.now().year}",
- "${clean}_${Random().nextInt(9999)}",
- ];
+ _isSuggestionLoading = true;
+ suggestions = [];
+ });
+
+ _debounce = Timer(const Duration(milliseconds: 500), () async {
+ final fetchedSuggestions = await ref
+ .read(authViewModelProvider.notifier)
+ .suggestUsernames(username: name);
+
+ if (mounted) {
+ setState(() {
+ suggestions = fetchedSuggestions.take(4).toList();
+ _isSuggestionLoading = false;
+ });
+ }
});
}
@@ -80,6 +105,8 @@ class _UsernameScreenState extends ConsumerState {
@override
void dispose() {
+ _debounce?.cancel();
+ _usernameController.removeListener(_validateForm);
_usernameController.dispose();
_isFormValid.dispose();
super.dispose();
@@ -101,12 +128,11 @@ class _UsernameScreenState extends ConsumerState {
backgroundColor: Palette.textPrimary,
),
);
- ref.read(authViewModelProvider.notifier).resetState();
}
});
final authState = ref.watch(authViewModelProvider);
- final isLoading = authState.isLoading;
+ final isFormSubmitting = authState.isLoading;
return Scaffold(
backgroundColor: Palette.background,
@@ -117,7 +143,7 @@ class _UsernameScreenState extends ConsumerState {
elevation: 0,
),
body: AbsorbPointer(
- absorbing: isLoading,
+ absorbing: isFormSubmitting,
child: Stack(
children: [
Center(
@@ -159,18 +185,10 @@ class _UsernameScreenState extends ConsumerState {
controller: _usernameController,
labelText: 'Username',
validator: usernameValidator,
- onChanged: (value) {
- if (value.isNotEmpty) {
- generateSuggestions(value);
- } else {
- setState(() {
- suggestions = [];
- });
- }
- },
),
- if (suggestions.isNotEmpty) ...[
- const SizedBox(height: 24),
+ if (suggestions.isNotEmpty &&
+ !_isSuggestionLoading) ...[
+ const SizedBox(height: 20),
Wrap(
spacing: 1,
children: suggestions.asMap().entries.map((
@@ -189,7 +207,7 @@ class _UsernameScreenState extends ConsumerState {
Container(
padding: const EdgeInsets.only(
left: 0,
- right: 10,
+ right: 8,
top: 5,
bottom: 5,
),
@@ -219,11 +237,11 @@ class _UsernameScreenState extends ConsumerState {
);
}).toList(),
),
- const SizedBox(height: 14),
+ const SizedBox(height: 4),
GestureDetector(
onTap: () {
- generateSuggestions(
- _usernameController.text,
+ _fetchSuggestions(
+ _usernameController.text.trim(),
);
},
child: const Text(
@@ -245,7 +263,7 @@ class _UsernameScreenState extends ConsumerState {
),
),
),
- if (isLoading)
+ if (isFormSubmitting)
Container(
color: Colors.black.withOpacity(0.5),
child: const Center(child: Loader()),
@@ -293,7 +311,6 @@ class _UsernameScreenState extends ConsumerState {
disabledBackgroundColor: Palette.textWhite.withOpacity(0.6),
foregroundColor: Palette.background,
disabledForegroundColor: Palette.border,
- minimumSize: const Size(0, 38),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
diff --git a/lib/features/auth/view/screens/Create_Account/Verification_Screen.dart b/lib/features/auth/view/screens/Create_Account/Verification_Screen.dart
index 337efb1..27970df 100644
--- a/lib/features/auth/view/screens/Create_Account/Verification_Screen.dart
+++ b/lib/features/auth/view/screens/Create_Account/Verification_Screen.dart
@@ -45,12 +45,14 @@ class _VerificationScreenState extends ConsumerState {
if (!_formKey.currentState!.validate()) {
return;
}
+ FocusManager.instance.primaryFocus?.unfocus();
ref
.read(authViewModelProvider.notifier)
.verifySignupEmail(email: email, code: _codeController.text.trim());
}
void _resendCode() {
+ FocusManager.instance.primaryFocus?.unfocus();
ref
.read(authViewModelProvider.notifier)
.createAccount(name: name, email: email, dateOfBirth: dateOfBirth);
@@ -82,100 +84,105 @@ class _VerificationScreenState extends ConsumerState {
context.goNamed(RouteConstants.passwordscreen);
authViewModel.resetState();
} else if (next.type == AuthStateType.error) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(
- next.message ?? 'Invalid verification code',
- style: TextStyle(color: Palette.background),
+ FocusManager.instance.primaryFocus?.unfocus();
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ next.message ?? 'Invalid verification code',
+ style: const TextStyle(color: Palette.background),
+ ),
+ backgroundColor: Palette.textWhite,
),
- backgroundColor: Palette.textWhite,
- ),
- );
+ );
+ }
authViewModel.resetState();
}
});
final authState = ref.watch(authViewModelProvider);
final isLoading = authState.isLoading;
- return Scaffold(
- backgroundColor: Palette.background,
- appBar: AppBar(
- title: buildXLogo(size: 36),
- centerTitle: true,
+ return GestureDetector(
+ onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
+ child: Scaffold(
backgroundColor: Palette.background,
- elevation: 0,
- ),
- body: AbsorbPointer(
- absorbing: isLoading,
-
- child: Stack(
- children: [
- Center(
- child: Container(
- width: double.infinity,
- height: double.infinity,
- decoration: BoxDecoration(color: Palette.background),
- child: Column(
- children: [
- Expanded(
- child: Form(
- key: _formKey,
- child: SingleChildScrollView(
- padding: const EdgeInsets.symmetric(
- horizontal: 32,
- vertical: 16,
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text(
- 'We sent you a code',
- style: TextStyle(
- fontSize: 28,
- fontWeight: FontWeight.w800,
- color: Palette.textWhite,
- ),
- ),
- const SizedBox(height: 12),
- Text(
- 'Enter it below to verify $email.',
- style: const TextStyle(
- fontSize: 15,
- color: Palette.textSecondary,
- ),
- ),
- const SizedBox(height: 32),
- CustomTextField(
- controller: _codeController,
- labelText: 'Verification code',
- keyboardType: TextInputType.number,
- validator: verificationCodeValidator,
- ),
- const SizedBox(height: 24),
- GestureDetector(
- onTap: _resendCode,
- child: const Text(
- 'Didn\'t receive an email?',
+ appBar: AppBar(
+ title: buildXLogo(size: 36),
+ centerTitle: true,
+ backgroundColor: Palette.background,
+ elevation: 0,
+ ),
+ body: AbsorbPointer(
+ absorbing: isLoading,
+ child: Stack(
+ children: [
+ Center(
+ child: Container(
+ width: double.infinity,
+ height: double.infinity,
+ decoration: BoxDecoration(color: Palette.background),
+ child: Column(
+ children: [
+ Expanded(
+ child: Form(
+ key: _formKey,
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 32,
+ vertical: 16,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'We sent you a code',
style: TextStyle(
+ fontSize: 28,
+ fontWeight: FontWeight.w800,
+ color: Palette.textWhite,
+ ),
+ ),
+ const SizedBox(height: 12),
+ Text(
+ 'Enter it below to verify $email.',
+ style: const TextStyle(
fontSize: 15,
- color: Palette.primary,
+ color: Palette.textSecondary,
+ ),
+ ),
+ const SizedBox(height: 32),
+ CustomTextField(
+ controller: _codeController,
+ labelText: 'Verification code',
+ keyboardType: TextInputType.number,
+ validator: verificationCodeValidator,
+ ),
+ const SizedBox(height: 24),
+ GestureDetector(
+ onTap: _resendCode,
+ child: const Text(
+ 'Didn\'t receive an email?',
+ style: TextStyle(
+ fontSize: 15,
+ color: Palette.primary,
+ ),
),
),
- ),
- ],
+ ],
+ ),
),
),
),
- ),
- _buildNextButton(isLoading),
- const SizedBox(height: 15),
- ],
+ _buildNextButton(isLoading),
+ const SizedBox(height: 15),
+ ],
+ ),
),
),
- ),
- if (isLoading)
- Container(color: Colors.black, child: const Loader()),
- ],
+ if (isLoading)
+ Container(color: Colors.black, child: const Loader()),
+ ],
+ ),
),
),
);
diff --git a/lib/features/auth/view/screens/Intro_Screen.dart b/lib/features/auth/view/screens/Intro_Screen.dart
index ccee92a..cdf523a 100644
--- a/lib/features/auth/view/screens/Intro_Screen.dart
+++ b/lib/features/auth/view/screens/Intro_Screen.dart
@@ -1,17 +1,13 @@
-// ignore_for_file: unused_import
-
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
import 'package:lite_x/core/theme/palette.dart';
import 'package:lite_x/core/view/widgets/Loader.dart';
-import 'package:lite_x/features/auth/repositories/auth_remote_repository.dart';
import 'package:lite_x/features/auth/view/widgets/buildTermsText.dart';
import 'package:lite_x/features/auth/view/widgets/buildXLogo.dart';
import 'package:lite_x/features/auth/view_model/auth_state.dart';
import 'package:lite_x/features/auth/view_model/auth_view_model.dart';
-import 'package:google_sign_in/google_sign_in.dart';
class IntroScreen extends ConsumerWidget {
const IntroScreen({super.key});
@@ -41,14 +37,11 @@ class IntroScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final size = MediaQuery.of(context).size;
-
- // Listen to auth state changes
ref.listen(authViewModelProvider, (previous, next) {
final authViewModel = ref.read(authViewModelProvider.notifier);
if (next.type == AuthStateType.authenticated) {
- context.goNamed(RouteConstants.homescreen);
- authViewModel.resetState();
+ context.goNamed(RouteConstants.setbirthdate);
} else if (next.type == AuthStateType.error) {
_showErrorToast(
context,
@@ -109,7 +102,7 @@ class IntroScreen extends ConsumerWidget {
),
SizedBox(height: size.height * 0.15),
_buildAuthButtons(context, ref),
- const SizedBox(height: 25),
+ const SizedBox(height: 20),
buildTermsText(),
const SizedBox(height: 5),
_buildLoginSection(context),
diff --git a/lib/features/auth/view/screens/Oauth/SetBirthdate.dart b/lib/features/auth/view/screens/Oauth/SetBirthdate.dart
new file mode 100644
index 0000000..446b3e8
--- /dev/null
+++ b/lib/features/auth/view/screens/Oauth/SetBirthdate.dart
@@ -0,0 +1,227 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:go_router/go_router.dart';
+import 'package:intl/intl.dart';
+import 'package:lite_x/core/routes/Route_Constants.dart';
+import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/utils.dart';
+import 'package:lite_x/core/view/widgets/Loader.dart';
+import 'package:lite_x/features/auth/view/widgets/CustomTextField.dart';
+import 'package:lite_x/features/auth/view/widgets/buildXLogo.dart';
+import 'package:lite_x/features/auth/view_model/auth_view_model.dart';
+import 'package:lite_x/features/auth/view_model/auth_state.dart';
+
+class Setbirthdate extends ConsumerStatefulWidget {
+ const Setbirthdate({super.key});
+
+ @override
+ ConsumerState createState() => _SetbirthdateState();
+}
+
+class _SetbirthdateState extends ConsumerState {
+ final _dobController = TextEditingController();
+ final _isFormValid = ValueNotifier(false);
+ @override
+ void initState() {
+ super.initState();
+ _dobController.addListener(_validateForm);
+ }
+
+ @override
+ void dispose() {
+ _dobController.dispose();
+ _isFormValid.dispose();
+ super.dispose();
+ }
+
+ void _validateForm() {
+ final isValid = _dobController.text.trim().isNotEmpty;
+ _isFormValid.value = isValid;
+ }
+
+ Future _selectDate(BuildContext context) async {
+ FocusScope.of(context).unfocus();
+
+ final picked = await showDatePicker(
+ context: context,
+ initialDate: DateTime(DateTime.now().year - 18),
+ firstDate: DateTime(1950),
+ lastDate: DateTime.now(),
+ builder: (context, child) {
+ return Theme(
+ data: ThemeData.dark().copyWith(
+ colorScheme: const ColorScheme.dark(
+ primary: Palette.primary,
+ onPrimary: Palette.textWhite,
+ surface: Palette.background,
+ onSurface: Palette.textWhite,
+ ),
+ dialogBackgroundColor: Palette.background,
+ ),
+ child: child!,
+ );
+ },
+ );
+
+ if (picked != null && mounted) {
+ _dobController.text = DateFormat('MM/dd/yyyy').format(picked);
+ }
+ }
+
+ void _handleSignUp() {
+ if (_dobController.text.trim().isEmpty) return;
+
+ FocusScope.of(context).unfocus();
+ ref
+ .read(authViewModelProvider.notifier)
+ .Setbirthdate(birthDate: _dobController.text.trim());
+ }
+
+ void _showErrorToast(String message) {
+ if (!mounted) return;
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ message,
+ textAlign: TextAlign.center,
+ style: const TextStyle(
+ color: Palette.textWhite,
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ backgroundColor: Palette.greycolor,
+ behavior: SnackBarBehavior.floating,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
+ margin: EdgeInsets.symmetric(
+ horizontal: MediaQuery.of(context).size.width * 0.15,
+ vertical: MediaQuery.of(context).size.height * 0.4,
+ ),
+ duration: const Duration(seconds: 3),
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ ref.listen(authViewModelProvider, (previous, next) async {
+ if (next.type == AuthStateType.success) {
+ context.goNamed(RouteConstants.UserNameScreen);
+ } else if (next.type == AuthStateType.error) {
+ _showErrorToast(next.message ?? 'An error occurred');
+ }
+ });
+
+ final authState = ref.watch(authViewModelProvider);
+ final isLoading = authState.isLoading;
+
+ return Stack(
+ children: [
+ Scaffold(
+ backgroundColor: Palette.background,
+ appBar: AppBar(
+ title: buildXLogo(size: 36),
+ centerTitle: true,
+ backgroundColor: Palette.background,
+ elevation: 0,
+ ),
+ body: AbsorbPointer(
+ absorbing: isLoading,
+ child: Column(
+ children: [
+ Expanded(
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 32,
+ vertical: 16,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ "What's your birth date?",
+ style: TextStyle(
+ fontSize: 28,
+ fontWeight: FontWeight.w800,
+ color: Palette.textWhite,
+ ),
+ ),
+ const SizedBox(height: 12),
+ Text(
+ "This won't be public.",
+ style: TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.w400,
+ color: Palette.textWhite.withOpacity(0.5),
+ ),
+ ),
+ const SizedBox(height: 32),
+ CustomTextField(
+ controller: _dobController,
+ labelText: 'Date of birth',
+ readOnly: true,
+ onTap: () => _selectDate(context),
+ validator: dobValidator,
+ ),
+ ],
+ ),
+ ),
+ ),
+ _buildSignUpButton(isLoading),
+ const SizedBox(height: 15),
+ ],
+ ),
+ ),
+ ),
+ if (isLoading)
+ Container(
+ color: Colors.black.withOpacity(0.5),
+ child: const Loader(),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildSignUpButton(bool isLoading) {
+ return Container(
+ padding: const EdgeInsets.all(10),
+ alignment: Alignment.centerRight,
+ child: ValueListenableBuilder(
+ valueListenable: _isFormValid,
+ builder: (context, isValid, child) {
+ return SizedBox(
+ width: 120,
+ child: ElevatedButton(
+ onPressed: (isValid && !isLoading) ? _handleSignUp : null,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Palette.textWhite,
+ disabledBackgroundColor: Palette.textWhite.withOpacity(0.5),
+ foregroundColor: Palette.background,
+ disabledForegroundColor: Palette.border,
+ minimumSize: const Size(0, 38),
+ ),
+ child: isLoading
+ ? const SizedBox(
+ width: 20,
+ height: 20,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ valueColor: AlwaysStoppedAnimation(
+ Palette.background,
+ ),
+ ),
+ )
+ : const Text(
+ 'Sign up',
+ style: TextStyle(
+ fontSize: 19,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/features/auth/view_model/auth_view_model.dart b/lib/features/auth/view_model/auth_view_model.dart
index e0286d8..8c49aa4 100644
--- a/lib/features/auth/view_model/auth_view_model.dart
+++ b/lib/features/auth/view_model/auth_view_model.dart
@@ -7,6 +7,7 @@ import 'package:lite_x/features/auth/repositories/auth_local_repository.dart';
import 'package:lite_x/features/auth/repositories/auth_remote_repository.dart';
import 'package:lite_x/features/auth/view_model/auth_state.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
+import 'package:lite_x/features/chat/repositories/chat_local_repository.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'auth_view_model.g.dart';
@@ -14,11 +15,13 @@ part 'auth_view_model.g.dart';
class AuthViewModel extends _$AuthViewModel {
late AuthRemoteRepository _authRemoteRepository;
late AuthLocalRepository _authLocalRepository;
+ late ChatLocalRepository _chatLocalRepository;
@override
AuthState build() {
_authRemoteRepository = ref.read(authRemoteRepositoryProvider);
_authLocalRepository = ref.read(authLocalRepositoryProvider);
+ _chatLocalRepository = ref.read(chatLocalRepositoryProvider);
Future(() async {
await Future.delayed(const Duration(milliseconds: 300));
await _checkAuthStatus();
@@ -185,9 +188,7 @@ class AuthViewModel extends _$AuthViewModel {
result.fold(
(failure) =>
print("FCM: Failed to register token: ${failure.message}"),
- (message) => print(
- "FCM: Token registered successfully: $message",
- ), // change later
+ (message) => print("FCM: Token registered successfully: $message"),
);
} else {
print('User declined or has not accepted permission');
@@ -210,6 +211,7 @@ class AuthViewModel extends _$AuthViewModel {
email: email,
password: password,
);
+ print("login result:$result");
result.fold((failure) => state = AuthState.error(failure.message), (
data,
) async {
@@ -220,6 +222,10 @@ class AuthViewModel extends _$AuthViewModel {
]);
ref.read(currentUserProvider.notifier).adduser(user);
state = AuthState.authenticated('Login successful');
+ if (!Platform.environment.containsKey('FLUTTER_TEST')) {
+ _registerFcmToken();
+ _listenForFcmTokenRefresh();
+ }
});
}
@@ -228,6 +234,7 @@ class AuthViewModel extends _$AuthViewModel {
try {
await _authLocalRepository.clearTokens();
await _authLocalRepository.clearUser();
+ await _chatLocalRepository.clearAll();
ref.read(currentUserProvider.notifier).clearUser();
state = AuthState.unauthenticated();
} catch (e) {
@@ -377,6 +384,54 @@ class AuthViewModel extends _$AuthViewModel {
);
}
+ //------------------------------------------------suggest-usernames-------------------------------------------//
+ Future> suggestUsernames({required String username}) async {
+ final result = await _authRemoteRepository.suggest_usernames(
+ username: username,
+ );
+ return result.fold((failure) {
+ print("Username suggestion failed: ${failure.message}");
+ return [];
+ }, (suggestions) => suggestions);
+ }
+
+ //-----------------------------------------------------------setbirthdate-------------------------------------------------------------------------//
+ Future Setbirthdate({required String birthDate}) async {
+ final currentUser = ref.read(currentUserProvider);
+ if (currentUser == null) {
+ state = AuthState.error('User not found');
+ return;
+ }
+
+ final parts = birthDate.split('/');
+ if (parts.length != 3) {
+ state = AuthState.error('Invalid date format');
+ return;
+ }
+
+ final month = parts[0];
+ final day = parts[1];
+ final year = parts[2];
+
+ final result = await _authRemoteRepository.setbirthdate(
+ day: day,
+ month: month,
+ year: year,
+ );
+
+ await result.fold(
+ (failure) async {
+ state = AuthState.error(failure.message);
+ },
+ (message) async {
+ final updated = currentUser.copyWith(dob: '$month/$day/$year');
+ ref.read(currentUserProvider.notifier).state = updated;
+
+ state = AuthState.success('Birthdate set successfully');
+ },
+ );
+ }
+
//-------------------------------------------------Token Management--------------------------------------------------------------------------------------//
String? getAccessToken() {
final tokens = _authLocalRepository.getTokens();
@@ -436,16 +491,29 @@ class AuthViewModel extends _$AuthViewModel {
final localPath = file.path;
final currentUser = ref.read(currentUserProvider);
+
if (currentUser != null) {
- final updatedUser = currentUser.copyWith(
- photo: mediaId,
- localProfilePhotoPath: localPath,
+ final updateResult = await _authRemoteRepository
+ .updateProfilePhoto(currentUser.id, mediaId);
+
+ updateResult.fold(
+ (failure) async {
+ state = AuthState.error("Uploaded but backend update failed");
+ },
+ (_) async {
+ final updatedUser = currentUser.copyWith(
+ photo: mediaId,
+ localProfilePhotoPath: localPath,
+ );
+
+ await _authLocalRepository.saveUser(updatedUser);
+ ref.read(currentUserProvider.notifier).adduser(updatedUser);
+
+ state = AuthState.success(
+ "Profile photo updated successfully",
+ );
+ },
);
-
- await _authLocalRepository.saveUser(updatedUser);
- ref.read(currentUserProvider.notifier).adduser(updatedUser);
-
- state = AuthState.success("Profile photo uploaded and saved");
}
},
);
@@ -491,6 +559,8 @@ class AuthViewModel extends _$AuthViewModel {
ref.read(currentUserProvider.notifier).adduser(user);
state = AuthState.authenticated('Social login successful');
+ _registerFcmToken();
+ _listenForFcmTokenRefresh();
},
);
}
diff --git a/lib/features/auth/view_model/auth_view_model.g.dart b/lib/features/auth/view_model/auth_view_model.g.dart
index 1ed2ed1..1109c12 100644
--- a/lib/features/auth/view_model/auth_view_model.g.dart
+++ b/lib/features/auth/view_model/auth_view_model.g.dart
@@ -41,7 +41,7 @@ final class AuthViewModelProvider
}
}
-String _$authViewModelHash() => r'73a355ed6a76f0c11f190556b7e016a5df57704f';
+String _$authViewModelHash() => r'924e5a98fc7048caca4ebb4164728767ec8beae9';
abstract class _$AuthViewModel extends $Notifier {
AuthState build();
diff --git a/lib/features/chat/models/conversationmodel.dart b/lib/features/chat/models/conversationmodel.dart
index 9b3d537..988b39f 100644
--- a/lib/features/chat/models/conversationmodel.dart
+++ b/lib/features/chat/models/conversationmodel.dart
@@ -14,31 +14,26 @@ class ConversationModel extends HiveObject {
DateTime createdAt;
@HiveField(3)
DateTime updatedAt;
+
@HiveField(4)
- String? groupName;
- @HiveField(5)
- String? groupPhotoKey;
- @HiveField(6)
- String? groupDescription;
- @HiveField(7)
List participantIds;
- @HiveField(8)
+ @HiveField(5)
String? lastMessageContent;
- @HiveField(9)
+ @HiveField(6)
DateTime? lastMessageTime;
- @HiveField(10)
+ @HiveField(7)
String? lastMessageSenderId;
- @HiveField(11)
+ @HiveField(8)
int unseenCount;
- @HiveField(12)
+ @HiveField(9)
String? dmPartnerUserId;
- @HiveField(13)
+ @HiveField(10)
String? dmPartnerName;
- @HiveField(14)
+ @HiveField(11)
String? dmPartnerUsername;
- @HiveField(15)
+ @HiveField(12)
String? dmPartnerProfileKey;
- @HiveField(16)
+ @HiveField(13)
String? lastMessageType;
ConversationModel({
@@ -46,14 +41,11 @@ class ConversationModel extends HiveObject {
required this.isDMChat,
required this.createdAt,
required this.updatedAt,
- this.groupName,
- this.groupPhotoKey,
- this.groupDescription,
required this.participantIds,
this.lastMessageContent,
this.lastMessageTime,
this.lastMessageSenderId,
- this.unseenCount = 0,
+ this.unseenCount = 1, //
this.dmPartnerUserId,
this.dmPartnerName,
this.dmPartnerUsername,
@@ -90,19 +82,6 @@ class ConversationModel extends HiveObject {
}
final unseenCount = json['unseenMessagesCount'] as int? ?? 0;
- Map? chatGroup;
-
- if (json['chatGroup'] is List) {
- if ((json['chatGroup'] as List).isNotEmpty) {
- chatGroup = (json['chatGroup'] as List).first;
- } else {
- chatGroup = null;
- }
- } else if (json['chatGroup'] is Map) {
- chatGroup = json['chatGroup'] as Map;
- } else {
- chatGroup = null;
- }
final isDm = json['DMChat'] as bool;
String? dmPartnerUserId;
@@ -129,9 +108,6 @@ class ConversationModel extends HiveObject {
isDMChat: isDm,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
- groupName: chatGroup?['name'] as String?,
- groupPhotoKey: chatGroup?['photo'] as String?,
- groupDescription: chatGroup?['description'] as String?,
participantIds: participantIds,
lastMessageContent: lastMessageContent,
lastMessageTime: lastMessageTime,
@@ -148,19 +124,11 @@ class ConversationModel extends HiveObject {
bool get isGroup => !isDMChat;
String getDisplayName() {
- if (isDMChat) {
- return dmPartnerName ?? dmPartnerUsername ?? "Unknown User";
- } else {
- return groupName ?? "Group Chat";
- }
+ return dmPartnerName ?? dmPartnerUsername ?? "Unknown User";
}
String? getDisplayImageKey() {
- if (isDMChat) {
- return dmPartnerProfileKey;
- } else {
- return groupPhotoKey;
- }
+ return dmPartnerProfileKey;
}
String? getOtherParticipantId(String currentUserId) {
@@ -173,7 +141,7 @@ class ConversationModel extends HiveObject {
@override
String toString() {
- return 'ConversationModel(id: $id, isDMChat: $isDMChat, createdAt: $createdAt, updatedAt: $updatedAt, groupName: $groupName, groupPhotoKey: $groupPhotoKey, groupDescription: $groupDescription, participantIds: $participantIds, lastMessageContent: $lastMessageContent, lastMessageTime: $lastMessageTime, lastMessageSenderId: $lastMessageSenderId, unseenCount: $unseenCount, dmPartnerUserId: $dmPartnerUserId, dmPartnerName: $dmPartnerName, dmPartnerUsername: $dmPartnerUsername, dmPartnerProfileKey: $dmPartnerProfileKey, lastMessageType: $lastMessageType)';
+ return 'ConversationModel(id: $id, isDMChat: $isDMChat, createdAt: $createdAt, updatedAt: $updatedAt, participantIds: $participantIds, lastMessageContent: $lastMessageContent, lastMessageTime: $lastMessageTime, lastMessageSenderId: $lastMessageSenderId, unseenCount: $unseenCount, dmPartnerUserId: $dmPartnerUserId, dmPartnerName: $dmPartnerName, dmPartnerUsername: $dmPartnerUsername, dmPartnerProfileKey: $dmPartnerProfileKey, lastMessageType: $lastMessageType)';
}
@override
@@ -185,9 +153,6 @@ class ConversationModel extends HiveObject {
other.isDMChat == isDMChat &&
other.createdAt == createdAt &&
other.updatedAt == updatedAt &&
- other.groupName == groupName &&
- other.groupPhotoKey == groupPhotoKey &&
- other.groupDescription == groupDescription &&
listEquals(other.participantIds, participantIds) &&
other.lastMessageContent == lastMessageContent &&
other.lastMessageTime == lastMessageTime &&
@@ -206,9 +171,6 @@ class ConversationModel extends HiveObject {
isDMChat.hashCode ^
createdAt.hashCode ^
updatedAt.hashCode ^
- groupName.hashCode ^
- groupPhotoKey.hashCode ^
- groupDescription.hashCode ^
participantIds.hashCode ^
lastMessageContent.hashCode ^
lastMessageTime.hashCode ^
@@ -245,9 +207,7 @@ class ConversationModel extends HiveObject {
isDMChat: isDMChat ?? this.isDMChat,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
- groupName: groupName ?? this.groupName,
- groupPhotoKey: groupPhotoKey ?? this.groupPhotoKey,
- groupDescription: groupDescription ?? this.groupDescription,
+
participantIds: participantIds ?? this.participantIds,
lastMessageContent: lastMessageContent ?? this.lastMessageContent,
lastMessageTime: lastMessageTime ?? this.lastMessageTime,
@@ -267,9 +227,7 @@ class ConversationModel extends HiveObject {
'isDMChat': isDMChat,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
- 'groupName': groupName,
- 'groupPhotoKey': groupPhotoKey,
- 'groupDescription': groupDescription,
+
'participantIds': participantIds,
'lastMessageContent': lastMessageContent,
'lastMessageTime': lastMessageTime?.millisecondsSinceEpoch,
@@ -289,13 +247,7 @@ class ConversationModel extends HiveObject {
isDMChat: map['isDMChat'] as bool,
createdAt: DateTime.parse(map['createdAt'] as String),
updatedAt: DateTime.parse(map['updatedAt'] as String),
- groupName: map['groupName'] != null ? map['groupName'] as String : null,
- groupPhotoKey: map['groupPhotoKey'] != null
- ? map['groupPhotoKey'] as String
- : null,
- groupDescription: map['groupDescription'] != null
- ? map['groupDescription'] as String
- : null,
+
participantIds: List.from(map['participantIds'] as List),
lastMessageContent: map['lastMessageContent'] != null
? map['lastMessageContent'] as String
diff --git a/lib/features/chat/models/conversationmodel.g.dart b/lib/features/chat/models/conversationmodel.g.dart
index 98a785d..25c8420 100644
--- a/lib/features/chat/models/conversationmodel.g.dart
+++ b/lib/features/chat/models/conversationmodel.g.dart
@@ -21,26 +21,23 @@ class ConversationModelAdapter extends TypeAdapter {
isDMChat: fields[1] as bool,
createdAt: fields[2] as DateTime,
updatedAt: fields[3] as DateTime,
- groupName: fields[4] as String?,
- groupPhotoKey: fields[5] as String?,
- groupDescription: fields[6] as String?,
- participantIds: (fields[7] as List).cast(),
- lastMessageContent: fields[8] as String?,
- lastMessageTime: fields[9] as DateTime?,
- lastMessageSenderId: fields[10] as String?,
- unseenCount: fields[11] == null ? 0 : (fields[11] as num).toInt(),
- dmPartnerUserId: fields[12] as String?,
- dmPartnerName: fields[13] as String?,
- dmPartnerUsername: fields[14] as String?,
- dmPartnerProfileKey: fields[15] as String?,
- lastMessageType: fields[16] as String?,
+ participantIds: (fields[4] as List).cast(),
+ lastMessageContent: fields[5] as String?,
+ lastMessageTime: fields[6] as DateTime?,
+ lastMessageSenderId: fields[7] as String?,
+ unseenCount: fields[8] == null ? 1 : (fields[8] as num).toInt(),
+ dmPartnerUserId: fields[9] as String?,
+ dmPartnerName: fields[10] as String?,
+ dmPartnerUsername: fields[11] as String?,
+ dmPartnerProfileKey: fields[12] as String?,
+ lastMessageType: fields[13] as String?,
);
}
@override
void write(BinaryWriter writer, ConversationModel obj) {
writer
- ..writeByte(17)
+ ..writeByte(14)
..writeByte(0)
..write(obj.id)
..writeByte(1)
@@ -50,30 +47,24 @@ class ConversationModelAdapter extends TypeAdapter {
..writeByte(3)
..write(obj.updatedAt)
..writeByte(4)
- ..write(obj.groupName)
- ..writeByte(5)
- ..write(obj.groupPhotoKey)
- ..writeByte(6)
- ..write(obj.groupDescription)
- ..writeByte(7)
..write(obj.participantIds)
- ..writeByte(8)
+ ..writeByte(5)
..write(obj.lastMessageContent)
- ..writeByte(9)
+ ..writeByte(6)
..write(obj.lastMessageTime)
- ..writeByte(10)
+ ..writeByte(7)
..write(obj.lastMessageSenderId)
- ..writeByte(11)
+ ..writeByte(8)
..write(obj.unseenCount)
- ..writeByte(12)
+ ..writeByte(9)
..write(obj.dmPartnerUserId)
- ..writeByte(13)
+ ..writeByte(10)
..write(obj.dmPartnerName)
- ..writeByte(14)
+ ..writeByte(11)
..write(obj.dmPartnerUsername)
- ..writeByte(15)
+ ..writeByte(12)
..write(obj.dmPartnerProfileKey)
- ..writeByte(16)
+ ..writeByte(13)
..write(obj.lastMessageType);
}
diff --git a/lib/features/chat/models/messagemodel.dart b/lib/features/chat/models/messagemodel.dart
index 48f50e6..8b1580b 100644
--- a/lib/features/chat/models/messagemodel.dart
+++ b/lib/features/chat/models/messagemodel.dart
@@ -50,6 +50,7 @@ class MessageModel extends HiveObject {
Map toApiRequest({List? recipientIds}) {
return {
+ "createdAt": createdAt.toIso8601String() + "Z",
"chatId": chatId,
"data": {"content": content},
if (recipientIds != null) "recipientId": recipientIds,
@@ -57,6 +58,21 @@ class MessageModel extends HiveObject {
}
factory MessageModel.fromApiResponse(Map json) {
+ final user = json['createdMessage']['user'] as Map?;
+ return MessageModel(
+ id: json['createdMessage']['id'] as String,
+ chatId: json['createdMessage']['chatId'] as String,
+ userId: json['createdMessage']['userId'] as String,
+ content: json['createdMessage']['content'] as String?,
+ createdAt: DateTime.parse(json['createdMessage']['createdAt'] as String),
+ status: json['createdMessage']['status'] as String? ?? 'PENDING',
+ senderUsername: user?['username'] as String?,
+ senderName: user?['name'] as String?,
+ senderProfileMediaKey: user?['profileMediaId'] as String?,
+ messageType: 'text',
+ );
+ }
+ factory MessageModel.fromLoadMessages(Map json) {
final user = json['user'] as Map?;
return MessageModel(
id: json['id'] as String,
@@ -71,7 +87,6 @@ class MessageModel extends HiveObject {
messageType: 'text',
);
}
-
MessageModel copyWith({
String? id,
String? chatId,
@@ -83,7 +98,6 @@ class MessageModel extends HiveObject {
String? senderName,
String? senderProfileMediaKey,
String? messageType,
- String? localId,
}) {
return MessageModel(
id: id ?? this.id,
diff --git a/lib/features/chat/models/usersearchmodel.dart b/lib/features/chat/models/usersearchmodel.dart
index 2806ff3..15369fa 100644
--- a/lib/features/chat/models/usersearchmodel.dart
+++ b/lib/features/chat/models/usersearchmodel.dart
@@ -32,13 +32,24 @@ class UserSearchModel extends HiveObject {
});
factory UserSearchModel.fromMap(Map map) {
+ String? profileImage;
+ if (map["profileMedia"] != null) {
+ if (map["profileMedia"] is Map) {
+ profileImage = map["profileMedia"]["keyName"];
+ } else if (map["profileMedia"] is String) {
+ profileImage = map["profileMedia"];
+ }
+ }
+
return UserSearchModel(
id: map["id"] ?? "",
username: map["username"] ?? "",
name: map["name"] ?? "",
bio: map["bio"],
- profileMedia: map["profileMedia"],
- followers: map["_count"]?["followers"] ?? 0,
+ profileMedia: profileImage,
+ followers: (map["_count"] != null && map["_count"]["followers"] != null)
+ ? map["_count"]["followers"]
+ : 0,
);
}
}
diff --git a/lib/features/chat/providers/activeChatIdProvider.dart b/lib/features/chat/providers/activeChatIdProvider.dart
new file mode 100644
index 0000000..59ca0bc
--- /dev/null
+++ b/lib/features/chat/providers/activeChatIdProvider.dart
@@ -0,0 +1,11 @@
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+part 'activeChatIdProvider.g.dart';
+
+@riverpod
+class ActiveChat extends _$ActiveChat {
+ @override
+ String? build() => null;
+
+ void setActive(String? id) => state = id;
+}
diff --git a/lib/features/search/view_model/search_view_model.g.dart b/lib/features/chat/providers/activeChatIdProvider.g.dart
similarity index 50%
rename from lib/features/search/view_model/search_view_model.g.dart
rename to lib/features/chat/providers/activeChatIdProvider.g.dart
index 8f79ec9..0fd91eb 100644
--- a/lib/features/search/view_model/search_view_model.g.dart
+++ b/lib/features/chat/providers/activeChatIdProvider.g.dart
@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
-part of 'search_view_model.dart';
+part of 'activeChatIdProvider.dart';
// **************************************************************************
// RiverpodGenerator
@@ -9,52 +9,51 @@ part of 'search_view_model.dart';
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
-@ProviderFor(SearchViewModel)
-const searchViewModelProvider = SearchViewModelProvider._();
+@ProviderFor(ActiveChat)
+const activeChatProvider = ActiveChatProvider._();
-final class SearchViewModelProvider
- extends $NotifierProvider {
- const SearchViewModelProvider._()
+final class ActiveChatProvider extends $NotifierProvider {
+ const ActiveChatProvider._()
: super(
from: null,
argument: null,
retry: null,
- name: r'searchViewModelProvider',
- isAutoDispose: false,
+ name: r'activeChatProvider',
+ isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
- String debugGetCreateSourceHash() => _$searchViewModelHash();
+ String debugGetCreateSourceHash() => _$activeChatHash();
@$internal
@override
- SearchViewModel create() => SearchViewModel();
+ ActiveChat create() => ActiveChat();
/// {@macro riverpod.override_with_value}
- Override overrideWithValue(SearchState value) {
+ Override overrideWithValue(String? value) {
return $ProviderOverride(
origin: this,
- providerOverride: $SyncValueProvider(value),
+ providerOverride: $SyncValueProvider(value),
);
}
}
-String _$searchViewModelHash() => r'3b9f4cb58d93e52298e4e42513a2cac8bee5cf50';
+String _$activeChatHash() => r'23f0395134ca4d8af5a88f7ad3482566e0783170';
-abstract class _$SearchViewModel extends $Notifier {
- SearchState build();
+abstract class _$ActiveChat extends $Notifier {
+ String? build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
- final ref = this.ref as $Ref;
+ final ref = this.ref as $Ref;
final element =
ref.element
as $ClassProviderElement<
- AnyNotifier,
- SearchState,
+ AnyNotifier,
+ String?,
Object?,
Object?
>;
diff --git a/lib/features/chat/providers/audiorecordernotifier.dart b/lib/features/chat/providers/audiorecordernotifier.dart
deleted file mode 100644
index 0ce103e..0000000
--- a/lib/features/chat/providers/audiorecordernotifier.dart
+++ /dev/null
@@ -1,166 +0,0 @@
-import 'package:riverpod_annotation/riverpod_annotation.dart';
-import 'package:lite_x/core/classes/AudioRecorderUtil.dart';
-import 'dart:io';
-import 'package:flutter/foundation.dart';
-
-part 'audiorecordernotifier.g.dart';
-
-enum RecorderStatus { idle, recording, reviewing }
-
-@riverpod
-class AudioRecorderNotifier extends _$AudioRecorderNotifier {
- final _recorder = AudioRecorderUtil();
- DateTime? _recordingStartTime;
- Duration? _actualRecordingDuration;
- static const maxDuration = Duration(seconds: 140);
-
- @override
- AudioRecorderState build() => const AudioRecorderState();
-
- Future startRecording() async {
- final success = await _recorder.startRecording();
- if (success) {
- _recordingStartTime = DateTime.now();
- _actualRecordingDuration = null;
- state = state.copyWith(
- status: RecorderStatus.recording,
- remainingDuration: maxDuration, // 140 seconds
- );
- }
- return success;
- }
-
- Future stopRecording() async {
- if (state.status != RecorderStatus.recording) return;
-
- final path = await _recorder.stopRecording();
- if (path == null) {
- await _resetToIdle();
- return;
- }
- final elapsed = _recordingStartTime != null
- ? DateTime.now().difference(_recordingStartTime!)
- : Duration.zero;
-
- _actualRecordingDuration = elapsed;
-
- state = state.copyWith(
- status: RecorderStatus.reviewing,
- recordingPath: path,
- remainingDuration: elapsed,
- );
- _recordingStartTime = null;
- }
-
- Future cancelRecording() async {
- if (state.status != RecorderStatus.recording) return;
-
- await _recorder.cancelRecording();
- await _resetToIdle();
- _recordingStartTime = null;
- _actualRecordingDuration = null;
- }
-
- Future cancelReview() async {
- if (state.status != RecorderStatus.reviewing) return;
-
- await _deleteRecordingFile();
- await _resetToIdle();
- _actualRecordingDuration = null;
- }
-
- String? sendRecording() {
- if (state.status != RecorderStatus.reviewing) return null;
-
- final path = state.recordingPath;
- _resetToIdle();
- _actualRecordingDuration = null;
- return path;
- }
-
- void updateRecordingDuration() {
- if (_recordingStartTime == null ||
- state.status != RecorderStatus.recording) {
- return;
- }
-
- final elapsed = DateTime.now().difference(_recordingStartTime!);
- final remaining = maxDuration - elapsed;
- if (remaining == Duration.zero || remaining.isNegative) {
- stopRecording();
- } else {
- state = state.copyWith(remainingDuration: remaining);
- }
- }
-
- void updateReviewPosition(Duration currentPosition) {
- if (state.status != RecorderStatus.reviewing ||
- _actualRecordingDuration == null) {
- return;
- }
- final remaining = _actualRecordingDuration! - currentPosition;
-
- if (remaining == Duration.zero || remaining.isNegative) {
- state = state.copyWith(remainingDuration: _actualRecordingDuration!);
- } else {
- state = state.copyWith(remainingDuration: remaining);
- }
- }
-
- void resetReviewPosition() {
- if (state.status == RecorderStatus.reviewing &&
- _actualRecordingDuration != null) {
- state = state.copyWith(remainingDuration: _actualRecordingDuration!);
- }
- }
-
- Future _deleteRecordingFile() async {
- try {
- final path = state.recordingPath;
- if (path == null) return;
- final file = File(path);
- if (await file.exists()) {
- await file.delete();
- }
- } catch (e) {
- debugPrint('Error deleting recording file: $e');
- }
- }
-
- Future _resetToIdle() async {
- state = const AudioRecorderState(
- status: RecorderStatus.idle,
- remainingDuration: Duration.zero,
- recordingPath: null,
- );
- }
-
- void dispose() {
- _recorder.dispose();
- }
-}
-
-class AudioRecorderState {
- final RecorderStatus status;
- final Duration remainingDuration;
- final String? recordingPath;
-
- const AudioRecorderState({
- this.status = RecorderStatus.idle,
- this.remainingDuration = Duration.zero,
- this.recordingPath,
- });
-
- AudioRecorderState copyWith({
- RecorderStatus? status,
- Duration? remainingDuration,
- String? recordingPath,
- bool clearPath = false,
- }) {
- return AudioRecorderState(
- status: status ?? this.status,
- remainingDuration: remainingDuration ?? this.remainingDuration,
- recordingPath: clearPath ? null : (recordingPath ?? this.recordingPath),
- );
- }
-}
diff --git a/lib/features/chat/providers/audiorecordernotifier.g.dart b/lib/features/chat/providers/audiorecordernotifier.g.dart
deleted file mode 100644
index c2f8adb..0000000
--- a/lib/features/chat/providers/audiorecordernotifier.g.dart
+++ /dev/null
@@ -1,64 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'audiorecordernotifier.dart';
-
-// **************************************************************************
-// RiverpodGenerator
-// **************************************************************************
-
-// GENERATED CODE - DO NOT MODIFY BY HAND
-// ignore_for_file: type=lint, type=warning
-
-@ProviderFor(AudioRecorderNotifier)
-const audioRecorderProvider = AudioRecorderNotifierProvider._();
-
-final class AudioRecorderNotifierProvider
- extends $NotifierProvider {
- const AudioRecorderNotifierProvider._()
- : super(
- from: null,
- argument: null,
- retry: null,
- name: r'audioRecorderProvider',
- isAutoDispose: true,
- dependencies: null,
- $allTransitiveDependencies: null,
- );
-
- @override
- String debugGetCreateSourceHash() => _$audioRecorderNotifierHash();
-
- @$internal
- @override
- AudioRecorderNotifier create() => AudioRecorderNotifier();
-
- /// {@macro riverpod.override_with_value}
- Override overrideWithValue(AudioRecorderState value) {
- return $ProviderOverride(
- origin: this,
- providerOverride: $SyncValueProvider(value),
- );
- }
-}
-
-String _$audioRecorderNotifierHash() =>
- r'43443ac8096028f1f2cb633063bff97bb266e55c';
-
-abstract class _$AudioRecorderNotifier extends $Notifier {
- AudioRecorderState build();
- @$mustCallSuper
- @override
- void runBuild() {
- final created = build();
- final ref = this.ref as $Ref;
- final element =
- ref.element
- as $ClassProviderElement<
- AnyNotifier,
- AudioRecorderState,
- Object?,
- Object?
- >;
- element.handleValue(ref, created);
- }
-}
diff --git a/lib/features/chat/repositories/chat_local_repository.dart b/lib/features/chat/repositories/chat_local_repository.dart
index 66690af..5d5669e 100644
--- a/lib/features/chat/repositories/chat_local_repository.dart
+++ b/lib/features/chat/repositories/chat_local_repository.dart
@@ -1,10 +1,7 @@
-// ignore_for_file: unused_import
-
import 'package:hive_ce/hive.dart';
import 'package:lite_x/features/chat/models/conversationmodel.dart';
import 'package:lite_x/features/chat/models/messagemodel.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
-
part 'chat_local_repository.g.dart';
@Riverpod(keepAlive: true)
@@ -16,72 +13,117 @@ class ChatLocalRepository {
final Box _conversationsBox = Hive.box(
"conversationsBox",
);
-
final Box _messagesBox = Hive.box("messagesBox");
- // Save or update conversations
- Future upsertConversations(
- List conversations,
- ) async {
- for (ConversationModel conv in conversations) {
- await _conversationsBox.put(conv.id, conv);
- }
- }
-
- // Load all cached conversations
- List getAllConversations() {
- return _conversationsBox.values.toList();
- }
+ List getCachedMessages(String chatId) {
+ final messages = _messagesBox.values
+ .where((msg) => msg.chatId == chatId)
+ .toList();
+ messages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
- //
- ConversationModel? getConversationById(String id) {
- return _conversationsBox.get(id);
+ return messages;
}
- // save Message
Future saveMessage(MessageModel message) async {
await _messagesBox.put(message.id, message);
+ if (message.status != "READ") {
+ await _enforceCacheLimit(message.chatId);
+ }
}
- // Save list of messages (when loading chat history)
- Future saveMessages(List messages) async {
- for (MessageModel msg in messages) {
- await _messagesBox.put(msg.id, msg);
+ Future saveInitialMessages(List messages) async {
+ final Map entries = {
+ for (var msg in messages) msg.id: msg,
+ };
+ await _messagesBox.putAll(entries);
+
+ if (messages.isNotEmpty) {
+ await _enforceCacheLimit(messages.first.chatId);
}
}
- // get messages by chat_id
- List getMessagesForChat(String chatId) {
- return _messagesBox.values.where((msg) => msg.chatId == chatId).toList();
+ Future replaceTempWithServerMessage({
+ required String tempId,
+ required MessageModel serverMessage,
+ }) async {
+ if (_messagesBox.containsKey(tempId)) {
+ await _messagesBox.delete(tempId);
+ }
+
+ await _messagesBox.put(serverMessage.id, serverMessage);
+ await _enforceCacheLimit(serverMessage.chatId);
}
- // update message status
Future markMessagesAsRead(String chatId, String myUserId) async {
- for (MessageModel msg in _messagesBox.values) {
- if (msg.chatId == chatId && msg.userId == myUserId) {
- msg.status = "READ";
- await msg.save();
- }
+ final messagesToUpdate = _messagesBox.values.where(
+ (msg) =>
+ msg.chatId == chatId &&
+ msg.userId == myUserId &&
+ msg.status != "READ",
+ );
+
+ for (var msg in messagesToUpdate) {
+ msg.status = "READ";
+ await msg.save();
}
}
- // When user sends a message -> mark as sent immediately
Future markMessageAsSent(String messageId) async {
final msg = _messagesBox.get(messageId);
- if (msg != null) {
+ if (msg != null && msg.status != "READ") {
msg.status = "SENT";
await msg.save();
}
}
- // DELETE MESSAGE
- Future deleteMessage(String messageId) async {
- await _messagesBox.delete(messageId);
+ Future _enforceCacheLimit(String chatId) async {
+ final chatMessages = _messagesBox.values
+ .where((msg) => msg.chatId == chatId)
+ .toList();
+
+ if (chatMessages.length <= 50) return;
+ chatMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
+
+ final messagesToDelete = chatMessages.sublist(50);
+ final keysToDelete = messagesToDelete.map((e) => e.id).toList();
+
+ if (keysToDelete.isNotEmpty) {
+ await _messagesBox.deleteAll(keysToDelete);
+ }
+ }
+
+ List getPendingMessages(String chatId) {
+ return _messagesBox.values
+ .where((msg) => msg.chatId == chatId && msg.status == "SENDING")
+ .toList();
+ }
+
+ Future upsertConversations(
+ List conversations,
+ ) async {
+ for (ConversationModel conv in conversations) {
+ await _conversationsBox.put(conv.id, conv);
+ }
+ }
+
+ List getAllConversations() {
+ return _conversationsBox.values.toList();
+ }
+
+ ConversationModel? getConversationById(String id) {
+ return _conversationsBox.get(id);
}
- // CLEAR ALL cache
Future clearAll() async {
await _conversationsBox.clear();
await _messagesBox.clear();
}
+
+ Future deleteMessage(String messageId) async {
+ await _messagesBox.delete(messageId);
+ }
+
+ Future deleteConversation(String conversationId) async {
+ await _conversationsBox.delete(conversationId);
+ }
}
diff --git a/lib/features/chat/repositories/chat_remote_repository.dart b/lib/features/chat/repositories/chat_remote_repository.dart
index fbc88c6..3ad5120 100644
--- a/lib/features/chat/repositories/chat_remote_repository.dart
+++ b/lib/features/chat/repositories/chat_remote_repository.dart
@@ -17,39 +17,6 @@ ChatRemoteRepository chatRemoteRepository(Ref ref) {
class ChatRemoteRepository {
final Dio _dio;
ChatRemoteRepository({required Dio dio}) : _dio = dio;
- //--------------------------------------------------search users to choose to chat with him or them according to group or not ----------------------------------------//
- Future>> searchUsers(
- String query,
- ) async {
- try {
- final response = await _dio.get(
- "api/users/search",
- queryParameters: {"query": query},
- );
-
- final data = response.data as Map;
-
- final List list = data["users"] ?? [];
-
- final users = list
- .map((e) => UserSearchModel.fromMap(e as Map))
- .toList();
-
- return Right(users);
- } on DioException catch (e) {
- print("DIO RESPONSE DATA: ${e.response?.data}");
- final errorMessage =
- e.response?.data["message"] ??
- e.response?.data["error"] ??
- "Failed to search users";
-
- return Left(AppFailure(message: errorMessage));
- } catch (e) {
- print("GENERAL ERROR: ${e.toString()}");
- return Left(AppFailure(message: e.toString()));
- }
- }
-
//----------------------------------------------------------------create chat ---------------------------------------------------------------------//
Future> create_chat({
required List recipientIds,
@@ -68,7 +35,7 @@ class ChatRemoteRepository {
data,
Current_UserId,
);
-
+ print(conversation.id);
return Right(conversation);
} on DioException catch (e) {
final errorMessage =
@@ -81,8 +48,8 @@ class ChatRemoteRepository {
}
}
- //----------------------------------------------------get inital messages from getchatinfo without timestemp-------------------------------//
- Future>> getInitialChatMessages(
+ //----------------------------------------------------get last 50 messages from getchatinfo without timestemp-------------------------------//
+ Future>> getlastChatMessages(
String chatId,
) async {
try {
@@ -93,7 +60,7 @@ class ChatRemoteRepository {
final List messagesList = data['messages'] ?? [];
final messages = messagesList
- .map((msg) => MessageModel.fromApiResponse(msg))
+ .map((msg) => MessageModel.fromLoadMessages(msg))
.toList();
return Right(messages);
@@ -137,52 +104,17 @@ class ChatRemoteRepository {
}
}
- //---------------------------------------------------------------update group info -------------------------------------------------------------------------------------//
- Future> updateGroupInfo({
- required String chatId,
- required String currentUserId,
- String? groupName,
- String? groupDescription,
- String? groupPhotoKey,
- }) async {
- try {
- final response = await _dio.put(
- "api/dm/chat/$chatId/group",
- data: {
- if (groupName != null) "name": groupName,
- if (groupDescription != null) "description": groupDescription,
- if (groupPhotoKey != null) "photo": groupPhotoKey,
- },
- );
-
- final data = response.data as Map;
- final updatedConversation = ConversationModel.fromApiResponse(
- data,
- currentUserId,
- );
-
- return Right(updatedConversation);
- } on DioException catch (e) {
- final errorMessage =
- e.response?.data["message"] ??
- e.response?.data["error"] ??
- "Failed to update group info";
- return Left(AppFailure(message: errorMessage));
- } catch (e) {
- return Left(AppFailure(message: e.toString()));
- }
- }
-
//-----------------------------------------------------------get messages of the conversation-------------------------------------------------------------------------//
- Future>> getMessagesChat(
- String chatId, {
+ Future>> getOlderMessagesChat({
+ required String chatId,
required DateTime lastMessageTimestamp,
}) async {
try {
final response = await _dio.get(
"api/dm/chat/$chatId/messages",
queryParameters: {
- "lastMessageTimestamp": lastMessageTimestamp.toIso8601String(),
+ "lastMessageTimestamp": lastMessageTimestamp.toIso8601String() + "Z",
+ "chatId": chatId,
},
);
@@ -190,7 +122,7 @@ class ChatRemoteRepository {
final messages = messagesList
.map(
- (msg) => MessageModel.fromApiResponse(msg as Map),
+ (msg) => MessageModel.fromLoadMessages(msg as Map),
)
.toList();
@@ -206,44 +138,35 @@ class ChatRemoteRepository {
}
}
- //----------------------------------------------------------------------get unseen count of one chat------------------------------------------------------------------------//
- Future> getUnseenCountOfChat(String chatId) async {
+ //--------------------------------------------------search users to choose to chat with him or them ----------------------------------------//
+ Future>> searchUsers(
+ String query,
+ ) async {
try {
final response = await _dio.get(
- "api/dm/chat/$chatId/unseen-messages-count",
+ "api/users/search",
+ queryParameters: {"query": query},
);
final data = response.data as Map;
- final unseenCount = data["unseenMessagesCount"] as int? ?? 0;
- return Right(unseenCount);
- } on DioException catch (e) {
- final errorMessage =
- e.response?.data["message"] ??
- e.response?.data["error"] ??
- "Failed to get unseen count";
- return Left(AppFailure(message: errorMessage));
- } catch (e) {
- return Left(AppFailure(message: e.toString()));
- }
- }
-
- //-----------------------------------------------------------------get unseen count of all messages of all chats --------------------------------------------------------------------------//
- Future> getUnseenCountAllChats() async {
- try {
- final response = await _dio.get("api/dm/chat/all-unseen-messages-count");
+ final List list = data["users"] ?? [];
- final data = response.data as Map;
- final totalUnseenCount = data["totalUnseenMessages"] as int? ?? 0;
+ final users = list
+ .map((e) => UserSearchModel.fromMap(e as Map))
+ .toList();
- return Right(totalUnseenCount);
+ return Right(users);
} on DioException catch (e) {
+ print("DIO RESPONSE DATA: ${e.response?.data}");
final errorMessage =
e.response?.data["message"] ??
e.response?.data["error"] ??
- "Failed to get all unseen count";
+ "Failed to search users";
+
return Left(AppFailure(message: errorMessage));
} catch (e) {
+ print("GENERAL ERROR: ${e.toString()}");
return Left(AppFailure(message: e.toString()));
}
}
@@ -275,19 +198,42 @@ class ChatRemoteRepository {
}
//------------------------------------------------------------------delete chat from conversions---------------------------------------------------------------------------//
- Future> deleteChat(String chatId) async {
+ Future> deleteChat(String chatId) async {
try {
final response = await _dio.delete("api/dm/chat/$chatId");
- final data = response.data as Map;
- final success = data["success"] as bool? ?? true;
+ if (response.statusCode == 200) {
+ final data = response.data as Map;
+ return Right(data["message"] ?? "Chat deleted successfully");
+ }
- return Right(success);
+ return Left(AppFailure(message: "Unexpected error"));
} on DioException catch (e) {
final errorMessage =
e.response?.data["message"] ??
e.response?.data["error"] ??
"Failed to delete chat";
+
+ return Left(AppFailure(message: errorMessage));
+ } catch (e) {
+ return Left(AppFailure(message: e.toString()));
+ }
+ }
+
+ //-----------------------------------------------------------------get unseen count of all messages of all chats --------------------------------------------------------------------------//
+ Future> getUnseenCountAllChats() async {
+ try {
+ final response = await _dio.get("api/dm/chat/all-unseen-messages-count");
+
+ final data = response.data as Map;
+ final totalUnseenCount = data["totalUnseenMessages"] as int? ?? 0;
+
+ return Right(totalUnseenCount);
+ } on DioException catch (e) {
+ final errorMessage =
+ e.response?.data["message"] ??
+ e.response?.data["error"] ??
+ "Failed to get all unseen count";
return Left(AppFailure(message: errorMessage));
} catch (e) {
return Left(AppFailure(message: e.toString()));
diff --git a/lib/features/chat/repositories/socket_repository.dart b/lib/features/chat/repositories/socket_repository.dart
index 8ab23df..2133003 100644
--- a/lib/features/chat/repositories/socket_repository.dart
+++ b/lib/features/chat/repositories/socket_repository.dart
@@ -1,14 +1,10 @@
-// ignore_for_file: unused_import, unused_field
-
-import 'package:dio/dio.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
-import 'package:lite_x/core/constants/server_constants.dart';
-import 'package:lite_x/core/models/TokensModel.dart';
-import 'package:lite_x/core/providers/dio_interceptor.dart';
+import 'package:lite_x/core/providers/unseenChatsCountProvider.dart';
import 'package:lite_x/features/auth/repositories/auth_local_repository.dart';
import 'package:lite_x/features/chat/providers/tokenStream.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:socket_io_client/socket_io_client.dart' as io;
+import 'dart:async';
part 'socket_repository.g.dart';
@Riverpod(keepAlive: true)
@@ -17,12 +13,33 @@ SocketRepository socketRepository(Ref ref) {
}
class SocketRepository {
+ final _newMessageController =
+ StreamController