From 8cedc50bbfdbb11ca59cf05d352e97b454e66cb7 Mon Sep 17 00:00:00 2001
From: asermohamed1 <153523890+asermohamed1@users.noreply.github.com>
Date: Sun, 30 Nov 2025 15:46:47 +0200
Subject: [PATCH 01/30] removed un implemented features and solve github , fcm
, and un seen count in chat
---
.env | 6 +
android/app/google-services.json | 21 +-
android/app/src/main/AndroidManifest.xml | 6 +-
firebase.json | 2 +-
lib/core/routes/AppRouter.dart | 9 -
lib/core/services/deep_link_service.dart | 1 -
lib/core/view/screen/app_shell.dart | 12 +-
.../repositories/auth_remote_repository.dart | 11 +-
.../auth/view/screens/Intro_Screen.dart | 12 +-
.../auth/view_model/auth_view_model.dart | 6 +
.../auth/view_model/auth_view_model.g.dart | 2 +-
.../chat/models/conversationmodel.dart | 82 +---
.../chat/models/conversationmodel.g.dart | 49 +--
lib/features/chat/models/messagemodel.dart | 2 +-
lib/features/chat/models/usersearchmodel.dart | 15 +-
.../chat/providers/audiorecordernotifier.dart | 166 -------
.../providers/audiorecordernotifier.g.dart | 64 ---
.../repositories/chat_remote_repository.dart | 102 ++---
.../view/screens/Search_Direct_messages.dart | 167 -------
.../chat/view/screens/Search_User_Group.dart | 79 +---
.../chat/view/screens/chat_Screen.dart | 3 -
.../widgets/chat/MessageOptionsSheet.dart | 2 -
.../view/widgets/chat/message_input_bar.dart | 408 +-----------------
.../view/widgets/conversion/SearchField.dart | 38 --
.../conversion/conversion_app_bar.dart | 13 +-
.../chat/view_model/chat/Chat_view_model.dart | 1 +
lib/firebase_options.dart | 64 ++-
lib/main.dart | 18 +-
28 files changed, 188 insertions(+), 1173 deletions(-)
create mode 100644 .env
delete mode 100644 lib/features/chat/providers/audiorecordernotifier.dart
delete mode 100644 lib/features/chat/providers/audiorecordernotifier.g.dart
delete mode 100644 lib/features/chat/view/screens/Search_Direct_messages.dart
delete mode 100644 lib/features/chat/view/widgets/conversion/SearchField.dart
diff --git a/.env b/.env
new file mode 100644
index 0000000..b56a8b7
--- /dev/null
+++ b/.env
@@ -0,0 +1,6 @@
+# API_URL=https://app-dbef67eb-9a2e-44fa-abff-3e8b83204d9c.cleverapps.io/
+# API_URL=https://node.shoy.publicvm.com/
+API_URL=https://aaf0fafe26af.ngrok-free.app/
+giphyApiKey=Ahjpgfo4LVqCACHRcwj0eoMlY5s7u1Uq
+Socket_Url=https://app-dbef67eb-9a2e-44fa-abff-3e8b83204d9c.cleverapps.io
+serverClientId=1096363232606-2fducjadk56bt4nsreqkj2jna7oiomga.apps.googleusercontent.com
\ No newline at end of file
diff --git a/android/app/google-services.json b/android/app/google-services.json
index 82e92bf..926b671 100644
--- a/android/app/google-services.json
+++ b/android/app/google-services.json
@@ -1,41 +1,34 @@
{
"project_info": {
- "project_number": "123824690535",
- "project_id": "litex-3c6f1",
- "storage_bucket": "litex-3c6f1.firebasestorage.app"
+ "project_number": "112144721859",
+ "project_id": "psychic-fin-474008-h8",
+ "storage_bucket": "psychic-fin-474008-h8.firebasestorage.app"
},
"client": [
{
"client_info": {
- "mobilesdk_app_id": "1:123824690535:android:f0760abf0029e802960bc2",
+ "mobilesdk_app_id": "1:112144721859:android:227c69fccfe2ec4c813f76",
"android_client_info": {
"package_name": "com.Artemsia.lite_x"
}
},
"oauth_client": [
{
- "client_id": "123824690535-52cesp7okt1d8j63pn7su9rtmi0asq0b.apps.googleusercontent.com",
+ "client_id": "112144721859-3i16bpjr6jd704h3imdsp4ojodv7t64l.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
- "current_key": "AIzaSyDYiU34-5-Rr2nP_SHphvLSIiOyr4RuC8I"
+ "current_key": "AIzaSyAPqcctNBpWkQ-BKvYsHUEjvc_iwPBwsZ0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
- "client_id": "123824690535-52cesp7okt1d8j63pn7su9rtmi0asq0b.apps.googleusercontent.com",
+ "client_id": "112144721859-3i16bpjr6jd704h3imdsp4ojodv7t64l.apps.googleusercontent.com",
"client_type": 3
- },
- {
- "client_id": "123824690535-5pta8j3mu07g0bb21n43n8q2vuulrstr.apps.googleusercontent.com",
- "client_type": 2,
- "ios_info": {
- "bundle_id": "com.example.litex"
- }
}
]
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index d445d4d..fe9021e 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -30,9 +30,9 @@
-
+
CustomTransitionPage(
- child: const SearchDirectMessages(),
- transitionsBuilder: _slideRightTransitionBuilder,
- ),
- ),
GoRoute(
name: RouteConstants.Interests,
path: "/Interests",
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/view/screen/app_shell.dart b/lib/core/view/screen/app_shell.dart
index 07eb9e2..68c9d36 100644
--- a/lib/core/view/screen/app_shell.dart
+++ b/lib/core/view/screen/app_shell.dart
@@ -32,7 +32,7 @@ class AppShell extends ConsumerWidget {
ExploreProfileScreen(),
_buildCommunitiesScreen(), // Index 2 - Communities
_buildNotificationsScreen(), // Index 3 - Notifications
- ConversationsScreen(), // Index 4 - Messages
+ ConversationsScreen(),
],
),
bottomNavigationBar: AnimatedContainer(
@@ -50,16 +50,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 8974e81..9b7b3da 100644
--- a/lib/features/auth/repositories/auth_remote_repository.dart
+++ b/lib/features/auth/repositories/auth_remote_repository.dart
@@ -32,10 +32,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 +49,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) {
@@ -59,7 +58,9 @@ class AuthRemoteRepository {
final decodedUser = Uri.decodeComponent(userRaw);
final user = UserModel.fromJson(decodedUser);
-
+ print("USER FROM GITHUB LOGIN: $user");
+ print("token FROM GITHUB LOGIN: $token");
+ print("refresh FROM GITHUB LOGIN: $refresh");
final tokens = TokensModel(
accessToken: token,
refreshToken: refresh,
@@ -339,7 +340,7 @@ class AuthRemoteRepository {
final user = UserModel.fromMap(response.data['user']);
final tokens = TokensModel.fromMap_login(response.data);
- // print(tokens);
+ // print("asermohamed${tokens.accessToken}");
return right((user, tokens));
} on DioException catch (e) {
return left(AppFailure(message: 'Login failed'));
diff --git a/lib/features/auth/view/screens/Intro_Screen.dart b/lib/features/auth/view/screens/Intro_Screen.dart
index ccee92a..6b6807a 100644
--- a/lib/features/auth/view/screens/Intro_Screen.dart
+++ b/lib/features/auth/view/screens/Intro_Screen.dart
@@ -3,6 +3,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
+import 'package:lite_x/core/providers/current_user_provider.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';
@@ -41,14 +42,17 @@ 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();
+ final currentUser = ref.read(currentUserProvider);
+ final bool hasInterests = currentUser?.interests.isNotEmpty ?? false;
+ if (hasInterests) {
+ context.goNamed(RouteConstants.homescreen);
+ } else {
+ context.goNamed(RouteConstants.Interests);
+ }
} else if (next.type == AuthStateType.error) {
_showErrorToast(
context,
diff --git a/lib/features/auth/view_model/auth_view_model.dart b/lib/features/auth/view_model/auth_view_model.dart
index e0286d8..16931f0 100644
--- a/lib/features/auth/view_model/auth_view_model.dart
+++ b/lib/features/auth/view_model/auth_view_model.dart
@@ -220,6 +220,10 @@ class AuthViewModel extends _$AuthViewModel {
]);
ref.read(currentUserProvider.notifier).adduser(user);
state = AuthState.authenticated('Login successful');
+ if (!Platform.environment.containsKey('FLUTTER_TEST')) {
+ _registerFcmToken();
+ _listenForFcmTokenRefresh();
+ }
});
}
@@ -491,6 +495,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..76dc6aa 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'd0ac1935764d4180e9fcb0ce510d61bea5fd7d9e';
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..a508719 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(),
"chatId": chatId,
"data": {"content": content},
if (recipientIds != null) "recipientId": recipientIds,
@@ -83,7 +84,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/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_remote_repository.dart b/lib/features/chat/repositories/chat_remote_repository.dart
index fbc88c6..4a0d532 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,
@@ -137,42 +104,6 @@ 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, {
@@ -228,6 +159,39 @@ class ChatRemoteRepository {
}
}
+ //--------------------------------------------------search users to choose to chat with him or them ----------------------------------------//
+ 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()));
+ }
+ }
+
//-----------------------------------------------------------------get unseen count of all messages of all chats --------------------------------------------------------------------------//
Future> getUnseenCountAllChats() async {
try {
diff --git a/lib/features/chat/view/screens/Search_Direct_messages.dart b/lib/features/chat/view/screens/Search_Direct_messages.dart
deleted file mode 100644
index b110631..0000000
--- a/lib/features/chat/view/screens/Search_Direct_messages.dart
+++ /dev/null
@@ -1,167 +0,0 @@
-// ignore_for_file: dead_code
-
-import 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:go_router/go_router.dart';
-import 'package:lite_x/core/theme/Palette.dart';
-
-class SearchDirectMessages extends ConsumerStatefulWidget {
- const SearchDirectMessages({super.key});
-
- @override
- ConsumerState createState() =>
- _SearchDirectMessagesState();
-}
-
-class _SearchDirectMessagesState extends ConsumerState {
- late final TextEditingController _searchController;
- String _searchQuery = '';
-
- @override
- void initState() {
- super.initState();
- _searchController = TextEditingController();
- _searchController.addListener(() {
- setState(() {
- _searchQuery = _searchController.text;
- });
- });
- }
-
- @override
- void dispose() {
- _searchController.dispose();
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- final bool isQueryEmpty = _searchQuery.isEmpty;
- final searchBarHintColor = Colors.grey[400];
-
- return DefaultTabController(
- length: 4,
- child: Scaffold(
- backgroundColor: Palette.background,
- appBar: AppBar(
- backgroundColor: Palette.background,
- elevation: 0,
- leading: IconButton(
- icon: const Icon(Icons.arrow_back, color: Colors.white),
- onPressed: () => context.pop(),
- ),
- title: TextField(
- controller: _searchController,
- autofocus: true,
- style: const TextStyle(color: Palette.textWhite, fontSize: 16),
-
- decoration: InputDecoration(
- hintText: isQueryEmpty ? 'Search Direct Messages' : '',
- hintStyle: TextStyle(color: searchBarHintColor, fontSize: 16),
- filled: false,
- border: InputBorder.none,
- focusedBorder: InputBorder.none,
- enabledBorder: InputBorder.none,
- suffixIcon: isQueryEmpty
- ? null
- : IconButton(
- icon: Icon(
- Icons.clear,
- color: Palette.inputBorderFocused,
- ),
- onPressed: () => _searchController.clear(),
- ),
- ),
- ),
- ),
- body: Column(
- children: [
- const Divider(color: Color(0xFF38444D), height: 0.5),
- if (!isQueryEmpty)
- Column(
- children: [
- TabBar(
- tabAlignment: TabAlignment.start,
- isScrollable: true,
- dividerColor: Colors.transparent,
- indicatorWeight: 1,
- indicatorColor: Palette.inputBorderFocused,
- labelColor: Palette.textWhite,
- unselectedLabelColor: searchBarHintColor,
- labelStyle: const TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 16,
- ),
- unselectedLabelStyle: const TextStyle(
- fontWeight: FontWeight.normal,
- ),
- tabs: const [
- Tab(text: 'All'),
- Tab(text: 'People'),
- Tab(text: 'Groups'),
- Tab(text: 'Messages'),
- ],
- ),
- ],
- ),
- const Divider(color: Color(0xFF38444D), height: 0.5),
- Expanded(
- child: isQueryEmpty
- ? _buildUnsearched(searchBarHintColor!)
- : _buildSearchResults(_searchQuery),
- ),
- ],
- ),
- ),
- );
- }
-
- Widget _buildUnsearched(Color hintColor) {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 17.0),
- child: Align(
- alignment: Alignment.topLeft,
- child: Text(
- 'Try searching for people, groups or messages',
- style: TextStyle(color: hintColor, fontSize: 16),
- ),
- ),
- );
- }
-
- Widget _buildSearchResults(String query) {
- return TabBarView(
- children: [
- _buildResultsList(query),
- _buildResultsList(query),
- _buildResultsList(query),
- _buildResultsList(query),
- ],
- );
- }
-
- Widget _buildResultsList(String query) {
- final bool hasResults = false;
-
- if (hasResults) {
- return ListView.builder(itemBuilder: (context, index) {});
- }
- return Center(
- child: Padding(
- padding: const EdgeInsets.all(16.0),
- child: Align(
- alignment: Alignment.bottomLeft,
- child: Text(
- 'No results for "$query"',
- textAlign: TextAlign.start,
- style: const TextStyle(
- color: Colors.white,
- fontSize: 30,
- fontWeight: FontWeight.bold,
- ),
- ),
- ),
- ),
- );
- }
-}
diff --git a/lib/features/chat/view/screens/Search_User_Group.dart b/lib/features/chat/view/screens/Search_User_Group.dart
index 27afc8d..98c707e 100644
--- a/lib/features/chat/view/screens/Search_User_Group.dart
+++ b/lib/features/chat/view/screens/Search_User_Group.dart
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart';
+import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
@@ -19,6 +20,7 @@ class SearchUserGroup extends ConsumerStatefulWidget {
class _SearchUserGroupState extends ConsumerState {
late final TextEditingController _searchController;
Timer? _debounce;
+ CancelToken? _cancelToken;
String _searchQuery = '';
bool _isGrouping = false;
@@ -35,18 +37,23 @@ class _SearchUserGroupState extends ConsumerState {
@override
void dispose() {
_debounce?.cancel();
+ _cancelToken?.cancel();
_searchController.dispose();
super.dispose();
}
void _onSearchChanged() {
- final query = _searchController.text;
-
- if (query == _searchQuery) return;
+ final rawQuery = _searchController.text;
+ final query = rawQuery.trim();
+ if (query == _searchQuery && rawQuery.isNotEmpty) return;
_searchQuery = query;
_debounce?.cancel();
- _debounce = Timer(const Duration(milliseconds: 300), () async {
+ if (_cancelToken != null && !_cancelToken!.isCancelled) {
+ _cancelToken!.cancel();
+ }
+
+ _debounce = Timer(const Duration(milliseconds: 600), () async {
if (!mounted) return;
if (query.isNotEmpty) {
final users = await ref
@@ -89,33 +96,6 @@ class _SearchUserGroupState extends ConsumerState {
}
}
- void _createGroup() async {
- if (_selectedUsers.isEmpty) return;
- final result = await ref
- .read(conversationsViewModelProvider.notifier)
- .createChat(
- isDMChat: false,
- recipientIds: _selectedUsers.map((u) => u.id).toList(),
- );
- result.fold(
- (failure) => ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(failure.message))),
- (chatModel) {
- context.pushNamed(
- RouteConstants.ChatScreen,
- pathParameters: {'chatId': chatModel.id},
- extra: {
- 'title': chatModel.groupName ?? "Group A",
- 'subtitle': "${_selectedUsers.length + 1} members",
- 'avatarUrl': chatModel.groupPhotoKey,
- 'isGroup': true,
- },
- );
- },
- );
- }
-
bool isValidHttpUrl(String? url) {
if (url == null) return false;
final uri = Uri.tryParse(url);
@@ -135,21 +115,7 @@ class _SearchUserGroupState extends ConsumerState {
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => context.pop(),
),
- actions: _isGrouping && _selectedUsers.isNotEmpty
- ? [
- TextButton(
- onPressed: _createGroup,
- child: Text(
- "Create",
- style: TextStyle(
- color: Palette.textWhite,
- fontSize: 16,
- fontWeight: FontWeight.w600,
- ),
- ),
- ),
- ]
- : null,
+ actions: null,
),
body: Column(
children: [
@@ -232,26 +198,7 @@ class _SearchUserGroupState extends ConsumerState {
),
),
const Divider(color: Color(0xFF38444D), height: 0.2),
- if (!_isGrouping)
- ListTile(
- leading: const Icon(
- Icons.groups_outlined,
- color: Palette.primary,
- ),
- title: Text(
- 'Create a group',
- style: TextStyle(
- color: Palette.primary,
- fontSize: 17,
- fontWeight: FontWeight.w600,
- ),
- ),
- onTap: () {
- setState(() {
- _isGrouping = true;
- });
- },
- ),
+
Expanded(
child: users.isEmpty
? Center(
diff --git a/lib/features/chat/view/screens/chat_Screen.dart b/lib/features/chat/view/screens/chat_Screen.dart
index f01a3f5..4b1317e 100644
--- a/lib/features/chat/view/screens/chat_Screen.dart
+++ b/lib/features/chat/view/screens/chat_Screen.dart
@@ -250,9 +250,6 @@ class _ChatScreenState extends ConsumerState {
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: MessageInputBar(
onSendMessage: _handleSendMessage,
- onSendAudio: null,
- onSendImage: null,
- onSendGif: null,
onTypingChanged: (isTyping) {
ref.read(chatViewModelProvider.notifier).sendTyping(isTyping);
},
diff --git a/lib/features/chat/view/widgets/chat/MessageOptionsSheet.dart b/lib/features/chat/view/widgets/chat/MessageOptionsSheet.dart
index 90b7c74..585017b 100644
--- a/lib/features/chat/view/widgets/chat/MessageOptionsSheet.dart
+++ b/lib/features/chat/view/widgets/chat/MessageOptionsSheet.dart
@@ -6,11 +6,9 @@ import 'package:lite_x/features/chat/models/messagemodel.dart';
class MessageOptionsSheet extends StatelessWidget {
final MessageModel message;
final bool isMe;
-
final VoidCallback? onDeleteForMe;
final VoidCallback? onDeleteForEveryone;
final VoidCallback? onEdit;
-
const MessageOptionsSheet({
super.key,
required this.message,
diff --git a/lib/features/chat/view/widgets/chat/message_input_bar.dart b/lib/features/chat/view/widgets/chat/message_input_bar.dart
index 5456cf9..0310b61 100644
--- a/lib/features/chat/view/widgets/chat/message_input_bar.dart
+++ b/lib/features/chat/view/widgets/chat/message_input_bar.dart
@@ -1,25 +1,15 @@
import 'dart:async';
-import 'package:just_audio/just_audio.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:lite_x/core/classes/PickedImage.dart';
-import 'package:lite_x/features/chat/providers/audiorecordernotifier.dart';
-import 'package:giphy_get/giphy_get.dart';
import 'package:lite_x/core/theme/palette.dart';
class MessageInputBar extends ConsumerStatefulWidget {
final Function(String text) onSendMessage;
- final Function(String)? onSendAudio;
- final Function(PickedImage)? onSendImage;
- final Function(String)? onSendGif;
final Function(bool isTyping)? onTypingChanged;
const MessageInputBar({
super.key,
required this.onSendMessage,
- this.onSendAudio,
- this.onSendImage,
- this.onSendGif,
+
this.onTypingChanged,
});
@@ -29,19 +19,12 @@ class MessageInputBar extends ConsumerStatefulWidget {
class _MessageInputBarState extends ConsumerState
with SingleTickerProviderStateMixin {
- final String _giphyApiKey = dotenv.env["giphyApiKey"] ?? "";
late AnimationController _colorController;
final TextEditingController _textController = TextEditingController();
Timer? _typingTimer;
bool _isTyping = false;
final FocusNode _focusNode = FocusNode();
- final AudioPlayer _audioPlayer = AudioPlayer();
-
- Timer? _recordingTimer;
- StreamSubscription? _playerStateSubscription;
- StreamSubscription? _positionSubscription;
- PickedImage? selectedImage;
@override
void initState() {
super.initState();
@@ -49,51 +32,17 @@ class _MessageInputBarState extends ConsumerState
vsync: this,
duration: const Duration(seconds: 1),
)..repeat(reverse: true);
- _setupAudioPlayer();
_textController.addListener(_onTextChanged);
}
- Future _stopPlayback() async {
- try {
- await _audioPlayer.stop();
- await _audioPlayer.seek(Duration.zero);
- ref.read(audioRecorderProvider.notifier).resetReviewPosition();
- } catch (e) {
- debugPrint("Error stopping playback: $e");
- }
- }
-
- void _setupAudioPlayer() {
- _playerStateSubscription = _audioPlayer.playerStateStream.listen((state) {
- if (!mounted) return;
-
- if (state.processingState == ProcessingState.completed) {
- _stopPlayback();
- }
- });
-
- _positionSubscription = _audioPlayer.positionStream.listen((position) {
- if (!mounted) return;
-
- final audioState = ref.read(audioRecorderProvider);
- if (audioState.status == RecorderStatus.reviewing &&
- _audioPlayer.playing) {
- ref.read(audioRecorderProvider.notifier).updateReviewPosition(position);
- }
- });
- }
-
@override
void dispose() {
- _recordingTimer?.cancel();
- _playerStateSubscription?.cancel();
- _positionSubscription?.cancel();
_typingTimer?.cancel();
_textController.removeListener(_onTextChanged);
_textController.dispose();
_colorController.dispose();
_focusNode.dispose();
- _audioPlayer.dispose();
+
super.dispose();
}
@@ -134,184 +83,22 @@ class _MessageInputBarState extends ConsumerState
_focusNode.requestFocus();
}
- Future _selectImage() async {
- selectedImage = await pickImage();
- if (selectedImage != null) {
- widget.onSendImage?.call(selectedImage!);
- }
- }
-
- Future _startRecording() async {
- try {
- final success = await ref
- .read(audioRecorderProvider.notifier)
- .startRecording();
- if (success) {
- _recordingTimer = Timer.periodic(const Duration(seconds: 1), (_) {
- if (mounted) {
- ref.read(audioRecorderProvider.notifier).updateRecordingDuration();
- }
- });
- } else {
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text(
- 'Failed to start recording. Please check microphone permissions.',
- ),
- backgroundColor: Palette.border,
- ),
- );
- }
- }
- } catch (e) {
- debugPrint('Error starting recording: $e');
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text('Recording error: $e'),
- backgroundColor: Palette.border,
- ),
- );
- }
- }
- }
-
- Future _stopRecording() async {
- try {
- _recordingTimer?.cancel();
- await ref.read(audioRecorderProvider.notifier).stopRecording();
- } catch (e) {
- debugPrint('Error stopping recording: $e');
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text('Error stopping recording: $e'),
- backgroundColor: Palette.border,
- ),
- );
- }
- }
- }
-
- Future _cancelRecording() async {
- try {
- _recordingTimer?.cancel();
- await ref.read(audioRecorderProvider.notifier).cancelRecording();
- } catch (e) {
- debugPrint('Error canceling recording: $e');
- }
- }
-
- Future _togglePlayback() async {
- final path = ref.read(audioRecorderProvider).recordingPath;
- if (path == null) return;
-
- try {
- if (_audioPlayer.playing) {
- await _audioPlayer.pause();
- } else {
- if (_audioPlayer.processingState == ProcessingState.completed ||
- _audioPlayer.processingState == ProcessingState.idle) {
- await _audioPlayer.setFilePath(path);
- }
- await _audioPlayer.play();
- }
- } catch (e) {
- debugPrint("Error toggling playback: $e");
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text('Playback error: $e'),
- backgroundColor: Palette.border,
- ),
- );
- }
- }
- }
-
- Future _cancelReview() async {
- await _stopPlayback();
- await ref.read(audioRecorderProvider.notifier).cancelReview();
- }
-
- Future _sendRecording() async {
- await _stopPlayback();
- final path = ref.read(audioRecorderProvider.notifier).sendRecording();
- if (path != null) {
- widget.onSendAudio?.call(path);
- }
- }
-
- Future _toggleGifPicker() async {
- final gif = await GiphyGet.getGif(
- context: context,
- apiKey: _giphyApiKey,
- lang: GiphyLanguage.english,
- tabColor: Palette.kBrandBlue,
- );
- if (gif != null) {
- final gifUrl = gif.images?.original?.url;
- if (gifUrl != null && gifUrl.isNotEmpty) {
- widget.onSendGif?.call(gifUrl);
- }
- }
- }
-
- String _formatRecordingDuration(Duration duration) {
- return '${duration.inSeconds}s';
- }
-
- String _formatReviewDuration(Duration duration) {
- String twoDigits(int n) => n.toString().padLeft(2, '0');
- final minutes = duration.inMinutes.remainder(60);
- final seconds = twoDigits(duration.inSeconds.remainder(60));
- return '$minutes:$seconds';
- }
-
@override
Widget build(BuildContext context) {
- final audioState = ref.watch(audioRecorderProvider);
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
- color: audioState.status == RecorderStatus.idle
- ? Palette.container_message_color
- : Colors.black,
+ color: Colors.black,
borderRadius: BorderRadius.circular(26),
),
- child: switch (audioState.status) {
- RecorderStatus.idle => _buildIdleView(theme),
- RecorderStatus.recording => _buildRecordingView(audioState, theme),
- RecorderStatus.reviewing => _buildReviewView(audioState, theme),
- },
+ child: _buildIdleView(theme),
);
}
Widget _buildIdleView(ThemeData theme) {
return Row(
children: [
- IconButton(
- icon: const Icon(
- Icons.image_outlined,
- color: Palette.kDimIconwhite,
- size: 26,
- ),
- onPressed: _selectImage,
- tooltip: 'Send image',
- constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
- ),
- IconButton(
- icon: const Icon(
- Icons.gif_box_outlined,
- color: Palette.kDimIconwhite,
- size: 26,
- ),
- onPressed: _toggleGifPicker,
- tooltip: 'Send GIF',
- constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
- ),
Expanded(
child: TextField(
maxLines: null,
@@ -333,7 +120,6 @@ class _MessageInputBarState extends ConsumerState
contentPadding: EdgeInsets.symmetric(horizontal: 2, vertical: 8),
),
style: const TextStyle(color: Color(0xFFFFFFFF), fontSize: 16),
- // onSubmitted: (_) => _handleSend(),
),
),
ValueListenableBuilder(
@@ -346,9 +132,8 @@ class _MessageInputBarState extends ConsumerState
color: hastext ? Palette.kBrandBlue : Palette.kBrandPurple,
size: 24,
),
- onPressed: hastext ? _handleSend : _startRecording,
- onLongPress: !hastext ? _startRecording : null,
- tooltip: hastext ? 'Send message' : 'Record audio',
+ onPressed: hastext ? _handleSend : null,
+ tooltip: hastext ? 'Send message' : null,
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
);
},
@@ -356,185 +141,4 @@ class _MessageInputBarState extends ConsumerState
],
);
}
-
- Widget _buildRecordingView(AudioRecorderState audioState, ThemeData theme) {
- return Container(
- color: Colors.black,
- padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- InkWell(
- onTap: _cancelRecording,
- child: const Text(
- 'Cancel',
- style: TextStyle(color: Palette.kDimIconwhite, fontSize: 14),
- ),
- ),
- Expanded(
- child: Container(
- height: 33,
- margin: const EdgeInsets.symmetric(horizontal: 10),
- padding: const EdgeInsets.symmetric(horizontal: 10),
- decoration: BoxDecoration(
- gradient: const LinearGradient(
- begin: Alignment.centerLeft,
- end: Alignment.centerRight,
- colors: [Color(0xFFD1C4F8), Color(0xFFE0D7FF)],
- ),
- borderRadius: BorderRadius.circular(15),
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Row(
- children: [
- AnimatedBuilder(
- animation: _colorController,
- builder: (context, child) {
- return Container(
- width: 10,
- height: 10,
- margin: const EdgeInsets.only(right: 5),
- decoration: BoxDecoration(
- color: Color.lerp(
- const Color(0xFFF04F78),
- const Color(0xFF8E24AA),
- _colorController.value,
- ),
- shape: BoxShape.circle,
- ),
- );
- },
- ),
- const Text(
- 'Recording',
- style: TextStyle(
- color: Color.fromARGB(255, 141, 108, 182),
- fontSize: 16,
- fontWeight: FontWeight.w600,
- ),
- ),
- ],
- ),
- Text(
- _formatRecordingDuration(audioState.remainingDuration),
- style: const TextStyle(
- color: Colors.white,
- fontSize: 14,
- fontWeight: FontWeight.w400,
- ),
- ),
- ],
- ),
- ),
- ),
- GestureDetector(
- onTap: _stopRecording,
- child: Container(
- width: 22,
- height: 22,
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- border: Border.all(
- color: const Color.fromARGB(255, 239, 57, 103),
- width: 2.2,
- ),
- ),
- child: const Center(
- child: Icon(
- size: 14,
- Icons.stop_rounded,
- color: Color(0xFFF04F78),
- ),
- ),
- ),
- ),
- ],
- ),
- );
- }
-
- Widget _buildReviewView(AudioRecorderState audioState, ThemeData theme) {
- return Container(
- color: Colors.black,
- padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- InkWell(
- onTap: _cancelReview,
- child: const Text(
- 'Cancel',
- style: TextStyle(color: Palette.kDimIconwhite, fontSize: 14),
- ),
- ),
- Expanded(
- child: Container(
- height: 33,
- margin: const EdgeInsets.symmetric(horizontal: 10),
- padding: const EdgeInsets.symmetric(horizontal: 10),
- decoration: BoxDecoration(
- gradient: const LinearGradient(
- begin: Alignment.centerLeft,
- end: Alignment.centerRight,
- colors: [
- Color.fromARGB(255, 169, 145, 255),
- Color.fromARGB(255, 163, 141, 244),
- ],
- ),
- borderRadius: BorderRadius.circular(15),
- ),
- child: GestureDetector(
- onTap: _togglePlayback,
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Row(
- children: [
- Icon(
- _audioPlayer.playing ? Icons.pause : Icons.play_arrow,
- color: Colors.white,
- size: 25,
- ),
- const SizedBox(width: 5),
- Text(
- _audioPlayer.playing ? 'Playing' : 'Play audio',
- style: const TextStyle(
- color: Colors.white,
- fontSize: 16,
- fontWeight: FontWeight.w500,
- ),
- ),
- ],
- ),
- Text(
- _formatReviewDuration(audioState.remainingDuration),
- style: const TextStyle(
- color: Colors.white,
- fontSize: 14,
- fontWeight: FontWeight.w500,
- ),
- ),
- ],
- ),
- ),
- ),
- ),
- GestureDetector(
- onTap: _sendRecording,
- child: Container(
- width: 30,
- height: 30,
- decoration: const BoxDecoration(
- color: Color(0xFF8A6BFE),
- shape: BoxShape.circle,
- ),
- child: const Icon(Icons.send, color: Colors.black, size: 18),
- ),
- ),
- ],
- ),
- );
- }
}
diff --git a/lib/features/chat/view/widgets/conversion/SearchField.dart b/lib/features/chat/view/widgets/conversion/SearchField.dart
deleted file mode 100644
index 933bcd3..0000000
--- a/lib/features/chat/view/widgets/conversion/SearchField.dart
+++ /dev/null
@@ -1,38 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:lite_x/core/theme/Palette.dart';
-
-class SearchField extends StatelessWidget {
- final String hintText;
- final VoidCallback? onTap;
-
- const SearchField({super.key, required this.hintText, this.onTap});
-
- @override
- Widget build(BuildContext context) {
- return GestureDetector(
- onTap: onTap,
- child: Container(
- margin: const EdgeInsets.symmetric(horizontal: 11),
- height: 40,
- decoration: BoxDecoration(
- color: Palette.cardBackground,
- borderRadius: BorderRadius.circular(24),
- ),
- padding: const EdgeInsets.symmetric(horizontal: 15),
- child: Row(
- children: [
- Expanded(
- child: Text(
- hintText,
- style: const TextStyle(
- color: Color.fromARGB(255, 85, 88, 92),
- fontSize: 18,
- ),
- ),
- ),
- ],
- ),
- ),
- );
- }
-}
diff --git a/lib/features/chat/view/widgets/conversion/conversion_app_bar.dart b/lib/features/chat/view/widgets/conversion/conversion_app_bar.dart
index 6902302..cb206d3 100644
--- a/lib/features/chat/view/widgets/conversion/conversion_app_bar.dart
+++ b/lib/features/chat/view/widgets/conversion/conversion_app_bar.dart
@@ -1,12 +1,8 @@
import 'dart:io';
-
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:go_router/go_router.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
-import 'package:lite_x/core/routes/Route_Constants.dart';
import 'package:lite_x/core/theme/Palette.dart';
-import 'package:lite_x/features/chat/view/widgets/conversion/SearchField.dart';
class ConversationAppBar extends ConsumerWidget implements PreferredSizeWidget {
const ConversationAppBar({super.key});
@@ -48,14 +44,7 @@ class ConversationAppBar extends ConsumerWidget implements PreferredSizeWidget {
),
),
),
- Expanded(
- child: SearchField(
- hintText: 'Search Direct Messages',
- onTap: () {
- context.pushNamed(RouteConstants.SearchDirectMessages);
- },
- ),
- ),
+ Spacer(),
IconButton(
icon: const Icon(
Icons.settings_outlined,
diff --git a/lib/features/chat/view_model/chat/Chat_view_model.dart b/lib/features/chat/view_model/chat/Chat_view_model.dart
index 6e7409f..66d8b1f 100644
--- a/lib/features/chat/view_model/chat/Chat_view_model.dart
+++ b/lib/features/chat/view_model/chat/Chat_view_model.dart
@@ -59,6 +59,7 @@ class ChatViewModel extends _$ChatViewModel {
void _setupSocketListeners() {
print("Setting up Socket Listeners in ViewModel...");
+
_socketRepository.onNewMessage((data) {
print("ViewModel Received Message Event");
if (_activeChatId == null) return;
diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart
index e5678ab..fcef8c6 100644
--- a/lib/firebase_options.dart
+++ b/lib/firebase_options.dart
@@ -41,52 +41,50 @@ class DefaultFirebaseOptions {
}
static const FirebaseOptions web = FirebaseOptions(
- apiKey: 'AIzaSyC6-I0IOtAmC9jnyIxf9WruRMUuc_VJqFo',
- appId: '1:123824690535:web:5c24b3d2f16411d1960bc2',
- messagingSenderId: '123824690535',
- projectId: 'litex-3c6f1',
- authDomain: 'litex-3c6f1.firebaseapp.com',
- storageBucket: 'litex-3c6f1.firebasestorage.app',
- measurementId: 'G-V5P21GBW6S',
+ apiKey: 'AIzaSyD5HN4nEV7xxsqLPufVptBwf2j-oXYVYCs',
+ appId: '1:112144721859:web:cd55e199eb8ef04e813f76',
+ messagingSenderId: '112144721859',
+ projectId: 'psychic-fin-474008-h8',
+ authDomain: 'psychic-fin-474008-h8.firebaseapp.com',
+ storageBucket: 'psychic-fin-474008-h8.firebasestorage.app',
+ measurementId: 'G-6XVXJKTZLW',
);
static const FirebaseOptions android = FirebaseOptions(
- apiKey: 'AIzaSyDYiU34-5-Rr2nP_SHphvLSIiOyr4RuC8I',
- appId: '1:123824690535:android:fc6ea2d45764d44a960bc2',
- messagingSenderId: '123824690535',
- projectId: 'litex-3c6f1',
- storageBucket: 'litex-3c6f1.firebasestorage.app',
+ apiKey: 'AIzaSyAPqcctNBpWkQ-BKvYsHUEjvc_iwPBwsZ0',
+ appId: '1:112144721859:android:227c69fccfe2ec4c813f76',
+ messagingSenderId: '112144721859',
+ projectId: 'psychic-fin-474008-h8',
+ storageBucket: 'psychic-fin-474008-h8.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
- apiKey: 'AIzaSyANjGmgYBumwUsVg_BIBYsD0SN_BdEZfFg',
- appId: '1:123824690535:ios:7d4e4fe266a006fe960bc2',
- messagingSenderId: '123824690535',
- projectId: 'litex-3c6f1',
- storageBucket: 'litex-3c6f1.firebasestorage.app',
- iosClientId:
- '123824690535-5pta8j3mu07g0bb21n43n8q2vuulrstr.apps.googleusercontent.com',
+ apiKey: 'AIzaSyDwLkRHFcciSTzy8dGANfbWzmUQ4kEKhCM',
+ appId: '1:112144721859:ios:990931c2c90bbd22813f76',
+ messagingSenderId: '112144721859',
+ projectId: 'psychic-fin-474008-h8',
+ storageBucket: 'psychic-fin-474008-h8.firebasestorage.app',
+ iosClientId: '112144721859-1i3tqk7isj7tf6733s8eje2fnj357k80.apps.googleusercontent.com',
iosBundleId: 'com.example.litex',
);
static const FirebaseOptions macos = FirebaseOptions(
- apiKey: 'AIzaSyANjGmgYBumwUsVg_BIBYsD0SN_BdEZfFg',
- appId: '1:123824690535:ios:7d4e4fe266a006fe960bc2',
- messagingSenderId: '123824690535',
- projectId: 'litex-3c6f1',
- storageBucket: 'litex-3c6f1.firebasestorage.app',
- iosClientId:
- '123824690535-5pta8j3mu07g0bb21n43n8q2vuulrstr.apps.googleusercontent.com',
+ apiKey: 'AIzaSyDwLkRHFcciSTzy8dGANfbWzmUQ4kEKhCM',
+ appId: '1:112144721859:ios:990931c2c90bbd22813f76',
+ messagingSenderId: '112144721859',
+ projectId: 'psychic-fin-474008-h8',
+ storageBucket: 'psychic-fin-474008-h8.firebasestorage.app',
+ iosClientId: '112144721859-1i3tqk7isj7tf6733s8eje2fnj357k80.apps.googleusercontent.com',
iosBundleId: 'com.example.litex',
);
static const FirebaseOptions windows = FirebaseOptions(
- apiKey: 'AIzaSyC6-I0IOtAmC9jnyIxf9WruRMUuc_VJqFo',
- appId: '1:123824690535:web:70728cc3cfda8de2960bc2',
- messagingSenderId: '123824690535',
- projectId: 'litex-3c6f1',
- authDomain: 'litex-3c6f1.firebaseapp.com',
- storageBucket: 'litex-3c6f1.firebasestorage.app',
- measurementId: 'G-ML4Y8SWB2Y',
+ apiKey: 'AIzaSyD5HN4nEV7xxsqLPufVptBwf2j-oXYVYCs',
+ appId: '1:112144721859:web:01d0e76c4bc65596813f76',
+ messagingSenderId: '112144721859',
+ projectId: 'psychic-fin-474008-h8',
+ authDomain: 'psychic-fin-474008-h8.firebaseapp.com',
+ storageBucket: 'psychic-fin-474008-h8.firebasestorage.app',
+ measurementId: 'G-7R40WH2L4T',
);
}
diff --git a/lib/main.dart b/lib/main.dart
index 17f8783..e9b3a47 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -19,16 +19,24 @@ void main() async {
Future init() async {
WidgetsFlutterBinding.ensureInitialized();
- await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
+
+ try {
+ await Firebase.initializeApp(
+ options: DefaultFirebaseOptions.currentPlatform,
+ );
+ } on FirebaseException catch (e) {
+ if (e.code != 'duplicate-app') {
+ rethrow;
+ }
+ }
+
DeepLinkService.init();
+
await Hive.initFlutter();
Hive.registerAdapter(UserModelAdapter());
Hive.registerAdapter(ConversationModelAdapter());
Hive.registerAdapter(MessageModelAdapter());
- // await Hive.deleteBoxFromDisk('userBox');
- // await Hive.deleteBoxFromDisk('tokenBox');
- // await Hive.deleteBoxFromDisk('conversationsBox');
- // await Hive.deleteBoxFromDisk('messagesBox');
+
await Hive.openBox('userBox');
await Hive.openBox('tokenBox');
await Hive.openBox('conversationsBox');
From 38297c98353760f5148e2e2050f9ec8160064297 Mon Sep 17 00:00:00 2001
From: asermohamed1 <153523890+asermohamed1@users.noreply.github.com>
Date: Sun, 30 Nov 2025 15:57:31 +0200
Subject: [PATCH 02/30] fix
---
lib/core/routes/AppRouter.dart | 16 +++++++--
lib/core/routes/Route_Constants.dart | 4 ++-
.../repositories/auth_remote_repository.dart | 33 ++++++++-----------
.../settings/screens/UserName_Screen.dart | 15 +++++----
4 files changed, 38 insertions(+), 30 deletions(-)
diff --git a/lib/core/routes/AppRouter.dart b/lib/core/routes/AppRouter.dart
index 41dd89f..80fae50 100644
--- a/lib/core/routes/AppRouter.dart
+++ b/lib/core/routes/AppRouter.dart
@@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
import 'package:lite_x/core/view/screen/app_shell.dart';
import 'package:lite_x/features/auth/view/screens/Create_Account/Interests.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
+import 'package:lite_x/features/auth/view/screens/Create_Account/UserName_Screen.dart';
import 'package:lite_x/features/auth/view/screens/Intro_Screen.dart';
import 'package:lite_x/core/view/screen/Splash_Screen.dart';
import 'package:lite_x/features/auth/view/screens/Create_Account/CreateAccount_Screen.dart';
@@ -16,14 +17,13 @@ import 'package:lite_x/features/auth/view/screens/Log_In/LoginPasswordScreen.dar
import 'package:lite_x/features/auth/view/screens/Log_In/Login_Screen.dart';
import 'package:lite_x/features/auth/view/screens/Create_Account/Password_Screen.dart';
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/UserName_Screen.dart'
- hide UsernameScreen;
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_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/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';
import 'package:lite_x/features/profile/view/screens/birthdate_screen.dart';
@@ -219,7 +219,7 @@ class Approuter {
name: RouteConstants.usernamesettings,
path: "/usernamesettings",
pageBuilder: (context, state) => CustomTransitionPage(
- child: const UsernameScreen(),
+ child: const UsernameSettings(),
transitionsBuilder: _slideRightTransitionBuilder,
),
),
@@ -434,6 +434,16 @@ class Approuter {
transitionsBuilder: _slideRightTransitionBuilder,
),
),
+ GoRoute(
+ name: RouteConstants.TweetDetailsScreen,
+ path: "/tweetDetailsScreen/:tweetId",
+ pageBuilder: (context, state) => CustomTransitionPage(
+ child: TweetDetailScreen(
+ tweetId: state.pathParameters['tweetId'] as String,
+ ),
+ transitionsBuilder: _slideRightTransitionBuilder,
+ ),
+ ),
],
redirect: (context, state) {
return null;
diff --git a/lib/core/routes/Route_Constants.dart b/lib/core/routes/Route_Constants.dart
index 657a76e..7dc85b6 100644
--- a/lib/core/routes/Route_Constants.dart
+++ b/lib/core/routes/Route_Constants.dart
@@ -18,7 +18,7 @@ class RouteConstants {
static String ChangePasswordFeedback = "ChangePasswordFeedback";
static String changePasswordScreen = "changePasswordScreen";
static String Interests = "Interests";
- static String usernamesettings = "UsernameScreen";
+ static String usernamesettings = "usernamesettings";
static String FollowingFollowersScreen = "FollowingFollowersScreen";
static String BirthDateScreen = "BirthDateScreen";
@@ -50,4 +50,6 @@ class RouteConstants {
// explore feature
static String ExploreScreen = "ExploreScreen";
static String ExploreProfileScreen = "ExploreProfileScreen";
+
+ static String TweetDetailsScreen = "TweetDetailsScreen";
}
diff --git a/lib/features/auth/repositories/auth_remote_repository.dart b/lib/features/auth/repositories/auth_remote_repository.dart
index 9b7b3da..a858d13 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';
@@ -58,9 +57,7 @@ class AuthRemoteRepository {
final decodedUser = Uri.decodeComponent(userRaw);
final user = UserModel.fromJson(decodedUser);
- print("USER FROM GITHUB LOGIN: $user");
- print("token FROM GITHUB LOGIN: $token");
- print("refresh FROM GITHUB LOGIN: $refresh");
+
final tokens = TokensModel(
accessToken: token,
refreshToken: refresh,
@@ -135,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()));
@@ -155,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()));
@@ -176,7 +173,7 @@ class AuthRemoteRepository {
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()));
@@ -236,7 +233,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()));
@@ -284,15 +281,13 @@ 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--------------------------------------------------------------------------//
-
//-------------------------------------------------FCM Token Registration-----------------------------------------------------------------------------------------//
Future> registerFcmToken({
required String fcmToken,
@@ -342,7 +337,7 @@ class AuthRemoteRepository {
final tokens = TokensModel.fromMap_login(response.data);
// 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"));
@@ -356,7 +351,7 @@ 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()));
@@ -374,7 +369,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()));
@@ -392,7 +387,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()));
@@ -411,7 +406,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()));
@@ -438,7 +433,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()));
@@ -456,7 +451,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()));
@@ -475,7 +470,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/settings/screens/UserName_Screen.dart b/lib/features/settings/screens/UserName_Screen.dart
index ef6ac1d..3f4d9c0 100644
--- a/lib/features/settings/screens/UserName_Screen.dart
+++ b/lib/features/settings/screens/UserName_Screen.dart
@@ -8,14 +8,15 @@ import 'package:lite_x/core/theme/palette.dart';
import 'package:lite_x/features/auth/view_model/auth_state.dart';
import 'package:lite_x/features/auth/view_model/auth_view_model.dart';
-class UsernameScreen extends ConsumerStatefulWidget {
- const UsernameScreen({super.key});
+class UsernameSettings extends ConsumerStatefulWidget {
+ const UsernameSettings({super.key});
@override
- ConsumerState createState() => _UsernameScreenState();
+ ConsumerState createState() =>
+ _UsernameSettingsState();
}
-class _UsernameScreenState extends ConsumerState {
+class _UsernameSettingsState extends ConsumerState {
late String currentUserName;
final TextEditingController _usernameController = TextEditingController();
final GlobalKey _formKey = GlobalKey();
@@ -41,8 +42,8 @@ class _UsernameScreenState extends ConsumerState {
String username = value.startsWith('@') ? value.substring(1) : value;
- if (username.length < 15) {
- return 'Username must be 15 characters or less ';
+ if (username.length < 5) {
+ return 'Username must be 5 characters or more ';
}
final RegExp usernameRegex = RegExp(r'^[a-zA-Z0-9_]+$');
@@ -63,7 +64,7 @@ class _UsernameScreenState extends ConsumerState {
}
setState(() {
- _isLoading = true;
+ if (mounted) _isLoading = true;
});
String newUsername = _usernameController.text.trim();
From 4936b060f6ed1a9e7b09f2516d629e6ab706c7cf Mon Sep 17 00:00:00 2001
From: asermohamed1 <153523890+asermohamed1@users.noreply.github.com>
Date: Sun, 30 Nov 2025 17:50:57 +0200
Subject: [PATCH 03/30] small fixes
---
.env | 2 +-
.../chat/providers/activeChatIdProvider.dart | 3 +
.../chat/view/screens/chat_Screen.dart | 4 +
.../view/widgets/chat/message_input_bar.dart | 14 ++--
.../chat/view/widgets/conversation_tile.dart | 0
.../chat/view_model/chat/Chat_view_model.dart | 9 ++-
.../conversions/Conversations_view_model.dart | 77 +++++++++++++++----
.../view/screens/reply_composer_screen.dart | 2 +-
.../media/repository/media_repo_impl.dart | 2 -
lib/features/media/test.dart | 2 -
.../profile/repositories/profile_repo.dart | 1 -
.../repositories/profile_repo_impl.dart | 1 -
.../screens/change_email_profile_screen.dart | 3 -
.../view/screens/explore_profile_screen.dart | 3 -
lib/main.dart | 5 --
15 files changed, 86 insertions(+), 42 deletions(-)
create mode 100644 lib/features/chat/providers/activeChatIdProvider.dart
delete mode 100644 lib/features/chat/view/widgets/conversation_tile.dart
diff --git a/.env b/.env
index b56a8b7..196d1ae 100644
--- a/.env
+++ b/.env
@@ -1,6 +1,6 @@
# API_URL=https://app-dbef67eb-9a2e-44fa-abff-3e8b83204d9c.cleverapps.io/
# API_URL=https://node.shoy.publicvm.com/
-API_URL=https://aaf0fafe26af.ngrok-free.app/
+API_URL=https://avah-pollinical-randal.ngrok-free.dev/
giphyApiKey=Ahjpgfo4LVqCACHRcwj0eoMlY5s7u1Uq
Socket_Url=https://app-dbef67eb-9a2e-44fa-abff-3e8b83204d9c.cleverapps.io
serverClientId=1096363232606-2fducjadk56bt4nsreqkj2jna7oiomga.apps.googleusercontent.com
\ No newline at end of file
diff --git a/lib/features/chat/providers/activeChatIdProvider.dart b/lib/features/chat/providers/activeChatIdProvider.dart
new file mode 100644
index 0000000..37a8633
--- /dev/null
+++ b/lib/features/chat/providers/activeChatIdProvider.dart
@@ -0,0 +1,3 @@
+import 'package:flutter_riverpod/legacy.dart';
+
+final activeChatProvider = StateProvider((ref) => null);
diff --git a/lib/features/chat/view/screens/chat_Screen.dart b/lib/features/chat/view/screens/chat_Screen.dart
index 4b1317e..5a50d1a 100644
--- a/lib/features/chat/view/screens/chat_Screen.dart
+++ b/lib/features/chat/view/screens/chat_Screen.dart
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/features/chat/models/messagemodel.dart';
+import 'package:lite_x/features/chat/providers/activeChatIdProvider.dart';
import 'package:lite_x/features/chat/view/widgets/chat/MessageAppBar.dart';
import 'package:lite_x/features/chat/view/widgets/chat/MessageBubble.dart';
@@ -57,6 +58,7 @@ class _ChatScreenState extends ConsumerState {
_setupChatSubscription();
ref.read(chatViewModelProvider.notifier).loadChat(widget.chatId);
+ ref.read(activeChatProvider.notifier).state = widget.chatId;
ref
.read(conversationsViewModelProvider.notifier)
.markChatAsRead(widget.chatId);
@@ -93,6 +95,8 @@ class _ChatScreenState extends ConsumerState {
@override
void dispose() {
+ ref.read(activeChatProvider.notifier).state = null;
+
_isExiting = true;
_chatSub?.close();
diff --git a/lib/features/chat/view/widgets/chat/message_input_bar.dart b/lib/features/chat/view/widgets/chat/message_input_bar.dart
index 0310b61..9f22514 100644
--- a/lib/features/chat/view/widgets/chat/message_input_bar.dart
+++ b/lib/features/chat/view/widgets/chat/message_input_bar.dart
@@ -87,9 +87,9 @@ class _MessageInputBarState extends ConsumerState
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
- padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
+ padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
- color: Colors.black,
+ color: Palette.container_message_color,
borderRadius: BorderRadius.circular(26),
),
child: _buildIdleView(theme),
@@ -117,7 +117,7 @@ class _MessageInputBarState extends ConsumerState
enabledBorder: InputBorder.none,
disabledBorder: InputBorder.none,
- contentPadding: EdgeInsets.symmetric(horizontal: 2, vertical: 8),
+ contentPadding: EdgeInsets.symmetric(horizontal: 2, vertical: 4),
),
style: const TextStyle(color: Color(0xFFFFFFFF), fontSize: 16),
),
@@ -127,14 +127,10 @@ class _MessageInputBarState extends ConsumerState
builder: (context, value, child) {
final hastext = value.text.trim().isNotEmpty;
return IconButton(
- icon: Icon(
- hastext ? Icons.send : Icons.graphic_eq,
- color: hastext ? Palette.kBrandBlue : Palette.kBrandPurple,
- size: 24,
- ),
+ icon: Icon(Icons.send, color: Palette.kBrandBlue, size: 24),
onPressed: hastext ? _handleSend : null,
tooltip: hastext ? 'Send message' : null,
- constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
+ constraints: const BoxConstraints(minWidth: 35, minHeight: 35),
);
},
),
diff --git a/lib/features/chat/view/widgets/conversation_tile.dart b/lib/features/chat/view/widgets/conversation_tile.dart
deleted file mode 100644
index e69de29..0000000
diff --git a/lib/features/chat/view_model/chat/Chat_view_model.dart b/lib/features/chat/view_model/chat/Chat_view_model.dart
index 66d8b1f..0b86e9c 100644
--- a/lib/features/chat/view_model/chat/Chat_view_model.dart
+++ b/lib/features/chat/view_model/chat/Chat_view_model.dart
@@ -4,6 +4,7 @@ import 'package:lite_x/features/chat/models/messagemodel.dart';
import 'package:lite_x/features/chat/repositories/chat_local_repository.dart';
import 'package:lite_x/features/chat/repositories/chat_remote_repository.dart';
import 'package:lite_x/features/chat/repositories/socket_repository.dart';
+import 'package:lite_x/features/chat/view_model/conversions/Conversations_view_model.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'Chat_view_model.g.dart';
@@ -261,7 +262,13 @@ class ChatViewModel extends _$ChatViewModel {
final updated = [...state.messages, localMessage];
state = state.copyWith(messages: updated);
}
-
+ ref
+ .read(conversationsViewModelProvider.notifier)
+ .updateConversationAfterSending(
+ chatId: localMessage.chatId,
+ content: localMessage.content ?? '',
+ messageType: localMessage.messageType,
+ );
_socketRepository.sendMessage(localMessage.toApiRequest());
}
diff --git a/lib/features/chat/view_model/conversions/Conversations_view_model.dart b/lib/features/chat/view_model/conversions/Conversations_view_model.dart
index 2ce0683..e76d8ea 100644
--- a/lib/features/chat/view_model/conversions/Conversations_view_model.dart
+++ b/lib/features/chat/view_model/conversions/Conversations_view_model.dart
@@ -1,5 +1,3 @@
-// ignore_for_file: unused_field
-
import 'package:fpdart/fpdart.dart';
import 'package:lite_x/core/classes/AppFailure.dart';
import 'package:lite_x/core/models/usermodel.dart';
@@ -7,6 +5,7 @@ import 'package:lite_x/core/providers/current_user_provider.dart';
import 'package:lite_x/features/chat/models/conversationmodel.dart';
import 'package:lite_x/features/chat/models/messagemodel.dart';
import 'package:lite_x/features/chat/models/usersearchmodel.dart';
+import 'package:lite_x/features/chat/providers/activeChatIdProvider.dart';
import 'package:lite_x/features/chat/repositories/chat_local_repository.dart';
import 'package:lite_x/features/chat/repositories/chat_remote_repository.dart';
import 'package:lite_x/features/chat/repositories/socket_repository.dart';
@@ -52,14 +51,20 @@ class ConversationsViewModel extends _$ConversationsViewModel {
final idx = currentConversations.indexWhere(
(chat) => chat.id == newMsg.chatId,
); // check if conversation already exist or not
-
+ final openChatId = ref.read(activeChatProvider);
+ final bool isChatOpen = openChatId == newMsg.chatId;
if (idx != -1) {
// conversation exists
final chat = currentConversations[idx];
final bool isMe = newMsg.userId == _currentUser?.id;
-
- final int newCount = isMe ? chat.unseenCount : (chat.unseenCount + 1);
-
+ final int newCount;
+ if (isMe) {
+ newCount = chat.unseenCount;
+ } else if (isChatOpen) {
+ newCount = 0;
+ } else {
+ newCount = chat.unseenCount + 1;
+ }
final updatedChat = chat.copyWith(
lastMessageContent: newMsg.content,
lastMessageType: newMsg.messageType,
@@ -71,6 +76,8 @@ class ConversationsViewModel extends _$ConversationsViewModel {
currentConversations[idx] = updatedChat;
} else {
//handle dm only
+ final bool isMe = newMsg.userId == _currentUser?.id;
+ final int initialUnseenCount = (isMe || isChatOpen) ? 0 : 1;
final created = ConversationModel(
id: newMsg.chatId,
isDMChat: true,
@@ -80,16 +87,14 @@ class ConversationsViewModel extends _$ConversationsViewModel {
newMsg.userId,
if (_currentUser != null) _currentUser!.id,
],
-
dmPartnerUserId: newMsg.userId,
dmPartnerName: newMsg.senderName ?? "Unknown",
dmPartnerUsername: newMsg.senderUsername,
dmPartnerProfileKey: newMsg.senderProfileMediaKey,
-
lastMessageContent: newMsg.content,
lastMessageType: newMsg.messageType,
lastMessageTime: newMsg.createdAt,
- unseenCount: (newMsg.userId == _currentUser?.id) ? 0 : 1,
+ unseenCount: initialUnseenCount,
lastMessageSenderId: newMsg.userId,
);
@@ -97,14 +102,25 @@ class ConversationsViewModel extends _$ConversationsViewModel {
_chatRemoteRepository
.getChatInfo(newMsg.chatId, _currentUser!.id)
.then((result) {
- result.fold((l) => null, (chat) async {
- await _chatLocalRepository.upsertConversations([chat]);
+ result.fold((l) => null, (serverChat) async {
+ final bool isMe = newMsg.userId == _currentUser!.id;
+ final int newCount = isMe ? 0 : 1;
+
+ final mergedChat = serverChat.copyWith(
+ unseenCount: newCount,
+ lastMessageContent: newMsg.content,
+ lastMessageType: newMsg.messageType,
+ lastMessageTime: newMsg.createdAt,
+ lastMessageSenderId: newMsg.userId,
+ );
+
+ await _chatLocalRepository.upsertConversations([mergedChat]);
final currentList = state.value ?? [];
final refreshed = List.from(currentList)
- ..removeWhere((c) => c.id == chat.id)
- ..add(chat);
+ ..removeWhere((c) => c.id == mergedChat.id)
+ ..add(mergedChat);
refreshed.sort((a, b) {
final aTime = a.lastMessageTime ?? a.updatedAt;
@@ -131,6 +147,41 @@ class ConversationsViewModel extends _$ConversationsViewModel {
});
}
+ void updateConversationAfterSending({
+ required String chatId,
+ required String content,
+ required String messageType,
+ }) {
+ if (_currentUser == null) return;
+
+ final currentList = state.value ?? [];
+ final index = currentList.indexWhere((c) => c.id == chatId);
+
+ if (index != -1) {
+ final chat = currentList[index];
+
+ final updatedChat = chat.copyWith(
+ lastMessageContent: content,
+ lastMessageType: messageType,
+ lastMessageTime: DateTime.now(),
+ lastMessageSenderId: _currentUser!.id,
+ unseenCount: 0,
+ );
+
+ final updatedList = List.from(currentList);
+ updatedList[index] = updatedChat;
+
+ updatedList.sort((a, b) {
+ final aTime = a.lastMessageTime ?? a.updatedAt;
+ final bTime = b.lastMessageTime ?? b.updatedAt;
+ return bTime.compareTo(aTime);
+ });
+
+ state = AsyncValue.data(updatedList);
+ _chatLocalRepository.upsertConversations([updatedChat]);
+ }
+ }
+
void markChatAsRead(String chatId) {
state.whenData((currentList) {
final updatedList = currentList.map((chat) {
diff --git a/lib/features/home/view/screens/reply_composer_screen.dart b/lib/features/home/view/screens/reply_composer_screen.dart
index 4faebf8..5af6a6e 100644
--- a/lib/features/home/view/screens/reply_composer_screen.dart
+++ b/lib/features/home/view/screens/reply_composer_screen.dart
@@ -407,7 +407,7 @@ class _ReplyComposerScreenState extends ConsumerState {
radius: 20,
backgroundColor: Colors.grey[800],
backgroundImage: userPhotoUrl != null
- ? NetworkImage(userPhotoUrl!)
+ ? NetworkImage(userPhotoUrl)
: null,
child: userPhotoUrl == null
? Icon(Icons.person, color: Colors.grey[600], size: 24)
diff --git a/lib/features/media/repository/media_repo_impl.dart b/lib/features/media/repository/media_repo_impl.dart
index 97112b2..705f901 100644
--- a/lib/features/media/repository/media_repo_impl.dart
+++ b/lib/features/media/repository/media_repo_impl.dart
@@ -2,8 +2,6 @@ import 'dart:io';
import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart';
-import 'package:lite_x/core/classes/AppFailure.dart';
-import 'package:lite_x/core/classes/PickedImage.dart';
import 'package:lite_x/features/profile/models/shared.dart';
import 'package:lite_x/features/media/models/confirm_upload_model.dart';
import 'package:lite_x/features/media/models/request_upload_model.dart';
diff --git a/lib/features/media/test.dart b/lib/features/media/test.dart
index 2520791..6e643ae 100644
--- a/lib/features/media/test.dart
+++ b/lib/features/media/test.dart
@@ -1,8 +1,6 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
-
-
void main() async {
List files = [];
files.add(File("./images.jpeg"));
diff --git a/lib/features/profile/repositories/profile_repo.dart b/lib/features/profile/repositories/profile_repo.dart
index 6b54929..2daa5a7 100644
--- a/lib/features/profile/repositories/profile_repo.dart
+++ b/lib/features/profile/repositories/profile_repo.dart
@@ -1,6 +1,5 @@
import 'package:lite_x/features/profile/models/create_reply_model.dart';
import 'package:lite_x/features/profile/models/create_tweet_model.dart';
-import 'package:lite_x/features/profile/models/follower_model.dart';
import 'package:lite_x/features/profile/models/profile_model.dart';
import 'package:lite_x/features/profile/models/profile_tweet_model.dart';
import 'package:lite_x/features/profile/models/search_user_model.dart';
diff --git a/lib/features/profile/repositories/profile_repo_impl.dart b/lib/features/profile/repositories/profile_repo_impl.dart
index c9411d7..e667c88 100644
--- a/lib/features/profile/repositories/profile_repo_impl.dart
+++ b/lib/features/profile/repositories/profile_repo_impl.dart
@@ -2,7 +2,6 @@ import 'package:dio/dio.dart';
import 'package:lite_x/features/media/download_media.dart';
import 'package:lite_x/features/profile/models/create_reply_model.dart';
import 'package:lite_x/features/profile/models/create_tweet_model.dart';
-import 'package:lite_x/features/profile/models/follower_model.dart';
import 'package:lite_x/features/profile/models/profile_model.dart';
import 'package:lite_x/features/profile/models/profile_tweet_model.dart';
import 'package:lite_x/features/profile/models/search_user_model.dart';
diff --git a/lib/features/profile/view/screens/change_email_profile_screen.dart b/lib/features/profile/view/screens/change_email_profile_screen.dart
index 32c1c85..6d80b94 100644
--- a/lib/features/profile/view/screens/change_email_profile_screen.dart
+++ b/lib/features/profile/view/screens/change_email_profile_screen.dart
@@ -2,9 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
-import 'package:lite_x/core/routes/Route_Constants.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:lite_x/features/profile/models/profile_model.dart';
import 'package:lite_x/features/profile/view_model/providers.dart';
diff --git a/lib/features/profile/view/screens/explore_profile_screen.dart b/lib/features/profile/view/screens/explore_profile_screen.dart
index b4ae017..52dc437 100644
--- a/lib/features/profile/view/screens/explore_profile_screen.dart
+++ b/lib/features/profile/view/screens/explore_profile_screen.dart
@@ -1,12 +1,9 @@
-import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
-import 'package:lite_x/core/routes/Route_Constants.dart';
import 'package:lite_x/features/profile/models/shared.dart';
import 'package:lite_x/features/profile/view/widgets/profile_search/explore_profile_screen_body.dart';
-import 'package:lite_x/features/profile/view_model/providers.dart';
class ExploreProfileScreen extends ConsumerStatefulWidget {
const ExploreProfileScreen({super.key});
diff --git a/lib/main.dart b/lib/main.dart
index e9b3a47..2a57a8f 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -19,7 +19,6 @@ void main() async {
Future init() async {
WidgetsFlutterBinding.ensureInitialized();
-
try {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
@@ -29,20 +28,16 @@ Future init() async {
rethrow;
}
}
-
DeepLinkService.init();
-
await Hive.initFlutter();
Hive.registerAdapter(UserModelAdapter());
Hive.registerAdapter(ConversationModelAdapter());
Hive.registerAdapter(MessageModelAdapter());
-
await Hive.openBox('userBox');
await Hive.openBox('tokenBox');
await Hive.openBox('conversationsBox');
await Hive.openBox('messagesBox');
await dotenv.load(fileName: ".env");
-
await Hive.openBox('search_history');
}
From 5ebe9d76d8b2e36dccd4f09e5c14294ab258406c Mon Sep 17 00:00:00 2001
From: asermohamed1 <153523890+asermohamed1@users.noreply.github.com>
Date: Thu, 4 Dec 2025 01:10:36 +0200
Subject: [PATCH 04/30] fixed the position and arrangement of chat
---
.env | 8 +-
lib/core/routes/AppRouter.dart | 2 +
.../repositories/auth_remote_repository.dart | 3 +-
lib/features/chat/models/messagemodel.dart | 2 +-
.../repositories/chat_local_repository.dart | 117 ++++--
.../repositories/chat_remote_repository.dart | 86 ++---
.../chat/repositories/socket_repository.dart | 12 +-
.../chat/view/screens/Search_User_Group.dart | 1 +
.../chat/view/screens/chat_Screen.dart | 301 +++++++++------
.../chat/view/widgets/chat/MessageAppBar.dart | 16 -
.../widgets/conversion/conversation_tile.dart | 3 +
.../chat/view_model/chat/Chat_view_model.dart | 354 ++++++++++--------
.../conversions/Conversations_view_model.dart | 6 +-
13 files changed, 522 insertions(+), 389 deletions(-)
diff --git a/.env b/.env
index 196d1ae..ef3aa58 100644
--- a/.env
+++ b/.env
@@ -1,6 +1,6 @@
# API_URL=https://app-dbef67eb-9a2e-44fa-abff-3e8b83204d9c.cleverapps.io/
# API_URL=https://node.shoy.publicvm.com/
-API_URL=https://avah-pollinical-randal.ngrok-free.dev/
-giphyApiKey=Ahjpgfo4LVqCACHRcwj0eoMlY5s7u1Uq
-Socket_Url=https://app-dbef67eb-9a2e-44fa-abff-3e8b83204d9c.cleverapps.io
-serverClientId=1096363232606-2fducjadk56bt4nsreqkj2jna7oiomga.apps.googleusercontent.com
\ No newline at end of file
+# API_URL=http://node.shoy.publicvm.com/
+API_URL=https://c14c6df1f8ff.ngrok-free.app/
+# Socket_Url=https://app-dbef67eb-9a2e-44fa-abff-3e8b83204d9c.cleverapps.io
+# serverClientId=1096363232606-2fducjadk56bt4nsreqkj2jna7oiomga.apps.googleusercontent.com
\ No newline at end of file
diff --git a/lib/core/routes/AppRouter.dart b/lib/core/routes/AppRouter.dart
index 80fae50..f5d8af5 100644
--- a/lib/core/routes/AppRouter.dart
+++ b/lib/core/routes/AppRouter.dart
@@ -357,6 +357,8 @@ class Approuter {
subtitle: extraData['subtitle'],
profileImage: extraData['avatarUrl'],
isGroup: extraData['isGroup'] ?? false,
+ recipientFollowersCount:
+ extraData['recipientFollowersCount'] ?? 0,
),
transitionsBuilder: _slideRightTransitionBuilder,
);
diff --git a/lib/features/auth/repositories/auth_remote_repository.dart b/lib/features/auth/repositories/auth_remote_repository.dart
index a858d13..e6de5a2 100644
--- a/lib/features/auth/repositories/auth_remote_repository.dart
+++ b/lib/features/auth/repositories/auth_remote_repository.dart
@@ -335,7 +335,8 @@ class AuthRemoteRepository {
final user = UserModel.fromMap(response.data['user']);
final tokens = TokensModel.fromMap_login(response.data);
- // print("asermohamed${tokens.accessToken}");
+ print("asermohamed${user.id}");
+ print("asermohamed${tokens.accessToken}");
return right((user, tokens));
} on DioException {
return left(AppFailure(message: 'Login failed'));
diff --git a/lib/features/chat/models/messagemodel.dart b/lib/features/chat/models/messagemodel.dart
index a508719..e4ea724 100644
--- a/lib/features/chat/models/messagemodel.dart
+++ b/lib/features/chat/models/messagemodel.dart
@@ -50,7 +50,7 @@ class MessageModel extends HiveObject {
Map toApiRequest({List? recipientIds}) {
return {
- "createdAt": createdAt.toIso8601String(),
+ "createdAt": createdAt.toIso8601String() + "Z",
"chatId": chatId,
"data": {"content": content},
if (recipientIds != null) "recipientId": recipientIds,
diff --git a/lib/features/chat/repositories/chat_local_repository.dart b/lib/features/chat/repositories/chat_local_repository.dart
index 6e69db9..e644440 100644
--- a/lib/features/chat/repositories/chat_local_repository.dart
+++ b/lib/features/chat/repositories/chat_local_repository.dart
@@ -1,5 +1,3 @@
-// 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';
@@ -16,72 +14,113 @@ 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);
- }
- }
+ List getCachedMessages(String chatId) {
+ final messages = _messagesBox.values
+ .where((msg) => msg.chatId == chatId)
+ .toList();
+ messages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
- // Load all cached conversations
- List getAllConversations() {
- return _conversationsBox.values.toList();
+ return messages;
}
- //
- ConversationModel? getConversationById(String id) {
- return _conversationsBox.get(id);
- }
-
- // 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);
+ }
}
diff --git a/lib/features/chat/repositories/chat_remote_repository.dart b/lib/features/chat/repositories/chat_remote_repository.dart
index 4a0d532..7fc2c54 100644
--- a/lib/features/chat/repositories/chat_remote_repository.dart
+++ b/lib/features/chat/repositories/chat_remote_repository.dart
@@ -35,7 +35,7 @@ class ChatRemoteRepository {
data,
Current_UserId,
);
-
+ print(conversation.id);
return Right(conversation);
} on DioException catch (e) {
final errorMessage =
@@ -48,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 {
@@ -105,15 +105,16 @@ class ChatRemoteRepository {
}
//-----------------------------------------------------------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,
},
);
@@ -137,28 +138,6 @@ class ChatRemoteRepository {
}
}
- //----------------------------------------------------------------------get unseen count of one chat------------------------------------------------------------------------//
- Future> getUnseenCountOfChat(String chatId) async {
- try {
- final response = await _dio.get(
- "api/dm/chat/$chatId/unseen-messages-count",
- );
-
- 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()));
- }
- }
-
//--------------------------------------------------search users to choose to chat with him or them ----------------------------------------//
Future>> searchUsers(
String query,
@@ -192,26 +171,6 @@ class ChatRemoteRepository {
}
}
- //-----------------------------------------------------------------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()));
- }
- }
-
//--------------------------------------------------------------get chat info --------------------------------------------------------------------------//
Future> getChatInfo(
String chatId,
@@ -239,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..19dca99 100644
--- a/lib/features/chat/repositories/socket_repository.dart
+++ b/lib/features/chat/repositories/socket_repository.dart
@@ -1,10 +1,4 @@
-// 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/features/auth/repositories/auth_local_repository.dart';
import 'package:lite_x/features/chat/providers/tokenStream.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -116,7 +110,7 @@ class SocketRepository {
void onNewMessage(Function(dynamic data) callback) {
_socket?.on("new-message", (data) {
callback(data);
- }); // receiver
+ });
}
// Mark chat opened (make all messages READ)
@@ -133,6 +127,10 @@ class SocketRepository {
_socket?.on("message-added", (data) => callback(data));
} //for sender
+ void leaveChat(String chatId) {
+ _socket?.emit("leave-chat", {"chatId": chatId});
+ } // when user leaves the chat screen used for unseen count
+
void disposeListeners() {
_socket?.off('new-message');
_socket?.off('user-typing');
diff --git a/lib/features/chat/view/screens/Search_User_Group.dart b/lib/features/chat/view/screens/Search_User_Group.dart
index 98c707e..4791cea 100644
--- a/lib/features/chat/view/screens/Search_User_Group.dart
+++ b/lib/features/chat/view/screens/Search_User_Group.dart
@@ -90,6 +90,7 @@ class _SearchUserGroupState extends ConsumerState {
'subtitle': "${user.username}",
'avatarUrl': user.profileMedia,
'isGroup': false,
+ 'recipientFollowersCount': user.followers,
},
);
});
diff --git a/lib/features/chat/view/screens/chat_Screen.dart b/lib/features/chat/view/screens/chat_Screen.dart
index 5a50d1a..e3b250c 100644
--- a/lib/features/chat/view/screens/chat_Screen.dart
+++ b/lib/features/chat/view/screens/chat_Screen.dart
@@ -2,9 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
import 'package:lite_x/core/theme/Palette.dart';
-import 'package:lite_x/features/chat/models/messagemodel.dart';
import 'package:lite_x/features/chat/providers/activeChatIdProvider.dart';
-
import 'package:lite_x/features/chat/view/widgets/chat/MessageAppBar.dart';
import 'package:lite_x/features/chat/view/widgets/chat/MessageBubble.dart';
import 'package:lite_x/features/chat/view/widgets/chat/MessageOptionsSheet.dart';
@@ -12,12 +10,11 @@ import 'package:lite_x/features/chat/view/widgets/chat/TypingIndicator.dart';
import 'package:lite_x/features/chat/view/widgets/chat/message_input_bar.dart';
import 'package:lite_x/features/chat/view_model/chat/Chat_view_model.dart';
import 'package:lite_x/features/chat/view_model/conversions/Conversations_view_model.dart';
-import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class ChatScreen extends ConsumerStatefulWidget {
final String chatId;
- final String title; // name of user or group name
- final String? subtitle; // username or group count
+ final String title; // name of user
+ final String? subtitle; // username
final String? profileImage;
final bool isGroup;
final int? recipientFollowersCount;
@@ -38,13 +35,14 @@ class ChatScreen extends ConsumerStatefulWidget {
class _ChatScreenState extends ConsumerState {
late String _currentUserId;
- final ItemPositionsListener _itemPositionsListener =
- ItemPositionsListener.create();
bool _isExiting = false;
bool _showScrollToBottomButton = false;
+
final ScrollController _scrollController = ScrollController();
- late ChatViewModel notifier;
+ bool _isLoadingMore = false;
+
+ late ChatViewModel notifier;
ProviderSubscription? _chatSub;
@override
@@ -52,19 +50,22 @@ class _ChatScreenState extends ConsumerState {
super.initState();
_currentUserId = ref.read(currentUserProvider)!.id;
notifier = ref.read(chatViewModelProvider.notifier);
+
+ _scrollController.addListener(_onScroll);
+
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_setupChatSubscription();
ref.read(chatViewModelProvider.notifier).loadChat(widget.chatId);
+
ref.read(activeChatProvider.notifier).state = widget.chatId;
+
ref
.read(conversationsViewModelProvider.notifier)
.markChatAsRead(widget.chatId);
});
-
- _itemPositionsListener.itemPositions.addListener(_scrollListener);
}
void _setupChatSubscription() {
@@ -72,27 +73,79 @@ class _ChatScreenState extends ConsumerState {
previous,
next,
) {
- if (_isExiting || !mounted) return;
+ if (!mounted || _isExiting) return;
final notifier = ref.read(chatViewModelProvider.notifier);
if (!notifier.isActiveChat(widget.chatId)) return;
- final positions = _itemPositionsListener.itemPositions.value;
- final isAtBottom =
- positions.isEmpty ||
- positions.any((pos) => pos.index == 0 && pos.itemLeadingEdge < 0.1);
+ final shouldAutoScroll = _shouldAutoScrollToBottom(previous, next);
- final lastMsg = next.messages.lastOrNull;
-
- if (lastMsg != null && (lastMsg.userId == _currentUserId || isAtBottom)) {
+ if (shouldAutoScroll) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_isExiting || !mounted) return;
- _scrollToBottom();
+ _scrollToBottom(animate: true);
});
}
}, fireImmediately: false);
}
+ bool _shouldAutoScrollToBottom(ChatState? previous, ChatState next) {
+ if (previous == null || previous.messages.isEmpty) return true;
+
+ if (previous.messages.length == next.messages.length) return false;
+
+ final lastMsg = next.messages.lastOrNull;
+ if (lastMsg == null) return false;
+
+ final isMine = lastMsg.userId == _currentUserId;
+ final isNearBottom = _isScrolledToBottom();
+
+ return isMine || isNearBottom;
+ }
+
+ bool _isScrolledToBottom() {
+ if (!_scrollController.hasClients) return true;
+
+ const threshold = 100.0;
+ final position = _scrollController.position;
+
+ return position.pixels <= threshold;
+ }
+
+ void _onScroll() {
+ if (_isExiting || !mounted) return;
+
+ final shouldShowButton = !_isScrolledToBottom();
+ if (shouldShowButton != _showScrollToBottomButton) {
+ setState(() {
+ _showScrollToBottomButton = shouldShowButton;
+ });
+ }
+
+ if (_scrollController.position.pixels >=
+ _scrollController.position.maxScrollExtent - 200 &&
+ !_isLoadingMore) {
+ _loadMoreMessages();
+ }
+ }
+
+ Future _loadMoreMessages() async {
+ if (_isLoadingMore) return;
+
+ final chatState = ref.read(chatViewModelProvider);
+ if (chatState.isLoadingHistory) return;
+
+ setState(() {
+ _isLoadingMore = true;
+ });
+
+ await ref.read(chatViewModelProvider.notifier).loadOlderMessages();
+
+ setState(() {
+ _isLoadingMore = false;
+ });
+ }
+
@override
void dispose() {
ref.read(activeChatProvider.notifier).state = null;
@@ -102,11 +155,11 @@ class _ChatScreenState extends ConsumerState {
_chatSub?.close();
_chatSub = null;
- _itemPositionsListener.itemPositions.removeListener(_scrollListener);
+ _scrollController.removeListener(_onScroll);
+ _scrollController.dispose();
try {
final savedNotifier = notifier;
-
Future.microtask(() {
savedNotifier.sendTyping(false);
savedNotifier.exitChat();
@@ -118,31 +171,19 @@ class _ChatScreenState extends ConsumerState {
super.dispose();
}
- void _scrollListener() {
- if (_isExiting || !mounted) return;
-
- final positions = _itemPositionsListener.itemPositions.value;
- if (positions.isEmpty) return;
-
- final isBottomVisible = positions.any((pos) => pos.index == 0);
- final shouldShowButton = !isBottomVisible;
-
- if (shouldShowButton != _showScrollToBottomButton) {
- setState(() {
- _showScrollToBottomButton = shouldShowButton;
- });
- }
- }
-
- void _scrollToBottom() {
+ void _scrollToBottom({bool animate = true}) {
if (!mounted || _isExiting) return;
if (!_scrollController.hasClients) return;
- _scrollController.animateTo(
- 0,
- duration: const Duration(milliseconds: 200),
- curve: Curves.easeOut,
- );
+ if (animate) {
+ _scrollController.animateTo(
+ 0,
+ duration: const Duration(milliseconds: 300),
+ curve: Curves.easeOut,
+ );
+ } else {
+ _scrollController.jumpTo(0);
+ }
}
void _handleSendMessage(String text) {
@@ -151,35 +192,16 @@ class _ChatScreenState extends ConsumerState {
final currentUser = ref.read(currentUserProvider);
if (currentUser == null || text.trim().isEmpty) return;
- final tempId = DateTime.now().millisecondsSinceEpoch.toString();
- final message = MessageModel(
- id: tempId,
- chatId: widget.chatId,
- userId: currentUser.id,
- content: text.trim(),
- createdAt: DateTime.now(),
- status: 'PENDING',
- messageType: 'text',
- );
-
- ref.read(chatViewModelProvider.notifier).sendMessage(message);
+ ref
+ .read(chatViewModelProvider.notifier)
+ .sendMessage(content: text.trim(), messageType: 'text');
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_isExiting || !mounted) return;
- _scrollToBottom();
+ _scrollToBottom(animate: true);
});
}
- void _handleDeleteMessage(MessageModel message, bool forEveryone) {
- print(
- 'Delete message ${message.id} for ${forEveryone ? "everyone" : "me"}',
- );
- }
-
- void _handleEditMessage(MessageModel message) {
- print('Edit message ${message.id}');
- }
-
@override
Widget build(BuildContext context) {
final chatState = ref.watch(chatViewModelProvider);
@@ -202,18 +224,33 @@ class _ChatScreenState extends ConsumerState {
Expanded(
child: Stack(
children: [
- if (chatState.isLoading)
+ if (chatState.isLoading && messages.isEmpty)
const Center(child: CircularProgressIndicator())
else
ListView.separated(
controller: _scrollController,
reverse: true,
- padding: const EdgeInsets.symmetric(vertical: 10),
- itemCount: messages.length + 1,
+ padding: const EdgeInsets.symmetric(
+ vertical: 10,
+ horizontal: 8,
+ ),
+ itemCount:
+ messages.length + (chatState.isLoadingHistory ? 2 : 1),
separatorBuilder: (_, __) => const SizedBox(height: 6),
itemBuilder: (context, index) {
- if (index == messages.length)
+ if (chatState.isLoadingHistory &&
+ index == messages.length + 1) {
+ return const Center(
+ child: Padding(
+ padding: EdgeInsets.all(16.0),
+ child: CircularProgressIndicator(),
+ ),
+ );
+ }
+
+ if (index == messages.length) {
return _buildProfileHeader();
+ }
final message = messages[messages.length - index - 1];
final isMe = message.userId == currentUser.id;
@@ -227,28 +264,37 @@ class _ChatScreenState extends ConsumerState {
context: context,
message: message,
isMe: isMe,
- onDeleteForMe: () =>
- _handleDeleteMessage(message, false),
- onDeleteForEveryone: () =>
- _handleDeleteMessage(message, true),
- onEdit: () => _handleEditMessage(message),
);
},
);
},
),
+
if (chatState.isRecipientTyping)
Positioned(
bottom: 0,
left: 0,
right: 0,
- child: TypingIndicator(userName: widget.title),
+ child: Container(
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ colors: [
+ Colors.transparent,
+ Theme.of(context).scaffoldBackgroundColor,
+ ],
+ ),
+ ),
+ child: TypingIndicator(userName: widget.title),
+ ),
),
_buildScrollToBottomButton(),
],
),
),
+
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
@@ -274,9 +320,9 @@ class _ChatScreenState extends ConsumerState {
CircleAvatar(
radius: 45,
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
- // backgroundImage: widget.profileImage != null
- // ? NetworkImage(widget.profileImage!)
- // : null,
+ backgroundImage: widget.profileImage != null
+ ? NetworkImage(widget.profileImage!)
+ : null,
child: widget.profileImage == null
? Text(
widget.title[0].toUpperCase(),
@@ -288,48 +334,50 @@ class _ChatScreenState extends ConsumerState {
)
: null,
),
- const SizedBox(height: 5),
+ const SizedBox(height: 8),
Text(
widget.title,
style: const TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w500,
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
color: Palette.textPrimary,
),
),
- if (widget.subtitle != null)
+
+ if (widget.subtitle != null) ...[
+ const SizedBox(height: 2),
Text(
'@${widget.subtitle}',
- style: TextStyle(
- fontSize: 16,
- color: Color.fromARGB(255, 133, 139, 145),
- ),
+ style: const TextStyle(fontSize: 15, color: Color(0xFF858B91)),
),
- const SizedBox(height: 12),
+ ],
- if (widget.recipientFollowersCount != null)
+ if (widget.recipientFollowersCount != null) ...[
+ const SizedBox(height: 8),
Text(
'${widget.recipientFollowersCount} Followers',
- style: TextStyle(fontSize: 14, color: Colors.grey[500]),
+ style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
+ ],
const SizedBox(height: 20),
Divider(
- thickness: 0.2,
- color: Colors.grey[500],
- indent: 10,
- endIndent: 10,
+ thickness: 0.5,
+ color: Colors.grey[300],
+ indent: 20,
+ endIndent: 20,
),
- const SizedBox(height: 4),
+
+ const SizedBox(height: 8),
Text(
- 'Today',
+ _getConversationStartDate(),
style: TextStyle(
- fontSize: 14,
- fontWeight: FontWeight.bold,
- color: Palette.textPrimary,
+ fontSize: 13,
+ fontWeight: FontWeight.w500,
+ color: Colors.grey[600],
),
),
],
@@ -337,20 +385,51 @@ class _ChatScreenState extends ConsumerState {
);
}
+ String _getConversationStartDate() {
+ final chatState = ref.read(chatViewModelProvider);
+ if (chatState.messages.isEmpty) return 'Today';
+
+ final oldestMessage = chatState.messages.first;
+ final now = DateTime.now();
+ final messageDate = oldestMessage.createdAt;
+
+ final difference = now.difference(messageDate);
+
+ if (difference.inDays == 0) {
+ return 'Today';
+ } else if (difference.inDays == 1) {
+ return 'Yesterday';
+ } else if (difference.inDays < 7) {
+ return '${difference.inDays} days ago';
+ } else {
+ return '${messageDate.day}/${messageDate.month}/${messageDate.year}';
+ }
+ }
+
Widget _buildScrollToBottomButton() {
return Positioned(
- bottom: 10.0,
- right: 10.0,
- child: AnimatedOpacity(
- opacity: _showScrollToBottomButton ? 1.0 : 0.0,
- duration: const Duration(milliseconds: 300),
- child: IgnorePointer(
- ignoring: !_showScrollToBottomButton,
- child: FloatingActionButton.small(
- onPressed: _scrollToBottom,
- tooltip: 'Scroll to bottom',
- backgroundColor: Theme.of(context).primaryColor,
- child: const Icon(Icons.arrow_downward, color: Colors.white),
+ bottom: 16.0,
+ right: 16.0,
+ child: AnimatedScale(
+ scale: _showScrollToBottomButton ? 1.0 : 0.0,
+ duration: const Duration(milliseconds: 200),
+ curve: Curves.easeInOut,
+ child: AnimatedOpacity(
+ opacity: _showScrollToBottomButton ? 1.0 : 0.0,
+ duration: const Duration(milliseconds: 200),
+ child: IgnorePointer(
+ ignoring: !_showScrollToBottomButton,
+ child: FloatingActionButton.small(
+ onPressed: () => _scrollToBottom(animate: true),
+ tooltip: 'Scroll to bottom',
+ backgroundColor: Theme.of(context).primaryColor,
+ elevation: 4,
+ child: const Icon(
+ Icons.keyboard_arrow_down_rounded,
+ color: Colors.white,
+ size: 24,
+ ),
+ ),
),
),
),
diff --git a/lib/features/chat/view/widgets/chat/MessageAppBar.dart b/lib/features/chat/view/widgets/chat/MessageAppBar.dart
index 3ca0097..8c6de0f 100644
--- a/lib/features/chat/view/widgets/chat/MessageAppBar.dart
+++ b/lib/features/chat/view/widgets/chat/MessageAppBar.dart
@@ -1,14 +1,11 @@
import 'package:flutter/material.dart';
import 'package:lite_x/core/theme/palette.dart';
-import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
class MessageAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final String? profileImage;
final String subtitle;
final VoidCallback? onProfileTap;
- final VoidCallback? onVideoCallTap;
- final VoidCallback? onAudioCallTap;
const MessageAppBar({
super.key,
@@ -16,8 +13,6 @@ class MessageAppBar extends StatelessWidget implements PreferredSizeWidget {
required this.subtitle,
this.profileImage,
this.onProfileTap,
- this.onVideoCallTap,
- this.onAudioCallTap,
});
@override
@@ -60,17 +55,6 @@ class MessageAppBar extends StatelessWidget implements PreferredSizeWidget {
],
),
),
- actions: [
- IconButton(
- icon: Icon(MdiIcons.videoPlusOutline, size: 28, color: Colors.white),
- onPressed: onVideoCallTap,
- ),
- IconButton(
- icon: Icon(MdiIcons.phoneOutline, size: 25, color: Colors.white),
- onPressed: onAudioCallTap,
- ),
- const SizedBox(width: 4),
- ],
);
}
}
diff --git a/lib/features/chat/view/widgets/conversion/conversation_tile.dart b/lib/features/chat/view/widgets/conversion/conversation_tile.dart
index 6b059fe..ba74753 100644
--- a/lib/features/chat/view/widgets/conversion/conversation_tile.dart
+++ b/lib/features/chat/view/widgets/conversion/conversation_tile.dart
@@ -14,6 +14,7 @@ class ConversationTile extends StatelessWidget {
final bool isUnread;
final int unseenCount;
final bool isDMChat;
+ final int recipientFollowersCount;
const ConversationTile({
super.key,
@@ -27,6 +28,7 @@ class ConversationTile extends StatelessWidget {
this.isUnread = false,
this.unseenCount = 0,
this.isDMChat = true,
+ this.recipientFollowersCount = 0,
});
@override
@@ -41,6 +43,7 @@ class ConversationTile extends StatelessWidget {
'avatarUrl': avatarUrl,
'subtitle': username,
'isGroup': isDMChat,
+ 'recipientFollowersCount': recipientFollowersCount,
},
);
},
diff --git a/lib/features/chat/view_model/chat/Chat_view_model.dart b/lib/features/chat/view_model/chat/Chat_view_model.dart
index 0b86e9c..b1c158d 100644
--- a/lib/features/chat/view_model/chat/Chat_view_model.dart
+++ b/lib/features/chat/view_model/chat/Chat_view_model.dart
@@ -6,28 +6,38 @@ import 'package:lite_x/features/chat/repositories/chat_remote_repository.dart';
import 'package:lite_x/features/chat/repositories/socket_repository.dart';
import 'package:lite_x/features/chat/view_model/conversions/Conversations_view_model.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
+import 'package:uuid/uuid.dart';
+
part 'Chat_view_model.g.dart';
class ChatState {
final List messages;
final bool isRecipientTyping;
final bool isLoading;
+ final bool isLoadingHistory;
+ final bool hasMoreHistory;
ChatState({
this.messages = const [],
this.isRecipientTyping = false,
this.isLoading = false,
+ this.isLoadingHistory = false,
+ this.hasMoreHistory = true,
});
ChatState copyWith({
List? messages,
bool? isRecipientTyping,
bool? isLoading,
+ bool? isLoadingHistory,
+ bool? hasMoreHistory,
}) {
return ChatState(
messages: messages ?? this.messages,
isRecipientTyping: isRecipientTyping ?? this.isRecipientTyping,
isLoading: isLoading ?? this.isLoading,
+ isLoadingHistory: isLoadingHistory ?? this.isLoadingHistory,
+ hasMoreHistory: hasMoreHistory ?? this.hasMoreHistory,
);
}
}
@@ -41,6 +51,9 @@ class ChatViewModel extends _$ChatViewModel {
String? _activeChatId;
String? _myUserId;
+ final List _historyBuffer = [];
+ final Set _loadedMessageIds = {};
+
@override
ChatState build() {
_chatRemoteRepository = ref.watch(chatRemoteRepositoryProvider);
@@ -53,74 +66,53 @@ class ChatViewModel extends _$ChatViewModel {
ref.onDispose(() {
_socketRepository.disposeListeners();
});
+
return ChatState(isLoading: false);
}
bool isActiveChat(String chatId) => _activeChatId == chatId;
void _setupSocketListeners() {
- print("Setting up Socket Listeners in ViewModel...");
-
_socketRepository.onNewMessage((data) {
- print("ViewModel Received Message Event");
- if (_activeChatId == null) return;
- if (data['chatId'] == _activeChatId) {
- handleIncomingMessage(data);
- }
+ print("New message received: $data");
+ _handleIncomingMessage(data);
});
+
_socketRepository.onMessageAdded((data) {
- if (_activeChatId == null) return;
- handleMessageAdded(data);
+ _handleMessageAck(data);
});
_socketRepository.onMessagesRead((data) {
- if (_activeChatId == null) return;
- handleMessagesRead(data);
+ _handleMessagesRead(data);
});
_socketRepository.onTyping((data) {
- if (_activeChatId == null) return;
- handleTypingEvent(data);
+ _handleTypingEvent(data);
});
}
- void handleMessageAdded(Map data) async {
- final chatId = data["chatId"];
- final realMessageId = data["messageId"];
- if (_activeChatId != chatId) return;
-
- final index = state.messages.indexWhere(
- (m) => m.status == "PENDING" && m.chatId == chatId,
- );
-
- if (index == -1) return;
-
- final tempMsg = state.messages[index];
-
- final updatedMsg = tempMsg.copyWith(id: realMessageId, status: "SENT");
- final updated = [...state.messages];
- updated[index] = updatedMsg;
- state = state.copyWith(messages: updated);
-
- _chatLocalRepository.saveMessage(updatedMsg);
+ List _sortMessages(List messages) {
+ final sorted = [...messages];
+ sorted.sort((a, b) => a.createdAt.compareTo(b.createdAt));
+ return sorted;
}
- // load messages (offline first)
Future loadChat(String chatId) async {
- if (_currentUser == null) {
- print("Error: No current user found");
- return;
- }
-
- state = state.copyWith(isLoading: true);
+ if (_currentUser == null) return;
+ state = state.copyWith(isLoading: true, hasMoreHistory: true);
_activeChatId = chatId;
_myUserId = _currentUser!.id;
+ _historyBuffer.clear();
+ _loadedMessageIds.clear();
- final localCached = _chatLocalRepository.getMessagesForChat(chatId);
- state = state.copyWith(messages: localCached);
+ final cachedMessages = _chatLocalRepository.getCachedMessages(chatId);
+ _loadedMessageIds.addAll(cachedMessages.map((m) => m.id));
- final result = await _chatRemoteRepository.getInitialChatMessages(chatId);
+ final sortedCache = _sortMessages(cachedMessages);
+ state = state.copyWith(messages: sortedCache);
+
+ final result = await _chatRemoteRepository.getlastChatMessages(chatId);
if (_activeChatId != chatId) {
state = state.copyWith(isLoading: false);
@@ -129,165 +121,222 @@ class ChatViewModel extends _$ChatViewModel {
await result.fold(
(failure) async {
- print("Failed to fetch messages: ${failure.message}");
state = state.copyWith(isLoading: false);
},
(serverMessages) async {
- final mergedMessages = await _mergeAndReconcileMessages(
- localCached,
- serverMessages,
- );
+ await _chatLocalRepository.saveInitialMessages(serverMessages);
+ await _reconcilePendingMessages(chatId, serverMessages);
- await _chatLocalRepository.saveMessages(serverMessages);
+ final updatedCache = _chatLocalRepository.getCachedMessages(chatId);
+ _loadedMessageIds.clear();
+ _loadedMessageIds.addAll(updatedCache.map((m) => m.id));
if (_activeChatId != chatId) {
state = state.copyWith(isLoading: false);
return;
}
- state = state.copyWith(messages: mergedMessages, isLoading: false);
+ final sortedUpdated = _sortMessages(updatedCache);
+ state = state.copyWith(messages: sortedUpdated, isLoading: false);
_socketRepository.openChat(chatId);
},
);
}
- //for matching messages not send while internet is off
- MessageModel? _findMatchingServerMessage(pending, serverMessages) {
- for (final srv in serverMessages) {
- if (srv.content == pending.content &&
- srv.chatId == pending.chatId &&
- srv.userId == pending.userId) {
- return srv;
- }
- }
- return null;
- }
-
- // reconcile the messages
- Future> _mergeAndReconcileMessages(
- List localMessages,
+ Future _reconcilePendingMessages(
+ String chatId,
List serverMessages,
) async {
- for (final srv in serverMessages) {
- await _chatLocalRepository.deleteMessage(srv.id);
- }
+ final pendingMessages = _chatLocalRepository.getPendingMessages(chatId);
+ if (pendingMessages.isEmpty) return;
- final Map messageMap = {};
-
- for (final srv in serverMessages) {
- messageMap[srv.id] = srv;
- await _chatLocalRepository.saveMessage(srv);
- }
-
- for (final localMsg in localMessages) {
- if (localMsg.status != "PENDING") continue;
-
- final match = _findMatchingServerMessage(localMsg, serverMessages);
+ for (final pending in pendingMessages) {
+ final match = _findMatchingServerMessage(pending, serverMessages);
if (match != null) {
- await _chatLocalRepository.deleteMessage(localMsg.id);
-
- messageMap[match.id] = match;
+ await _chatLocalRepository.replaceTempWithServerMessage(
+ tempId: pending.id,
+ serverMessage: match,
+ );
} else {
- messageMap[localMsg.id] = localMsg;
- await _chatLocalRepository.saveMessage(localMsg);
+ _socketRepository.sendMessage(pending.toApiRequest());
}
}
+ }
- final merged = messageMap.values.toList();
- merged.sort((a, b) => a.createdAt.compareTo(b.createdAt));
-
- return merged;
+ MessageModel? _findMatchingServerMessage(
+ MessageModel pending,
+ List serverMessages,
+ ) {
+ return serverMessages.cast().firstWhere(
+ (srv) =>
+ srv!.content == pending.content &&
+ srv.chatId == pending.chatId &&
+ srv.userId == pending.userId &&
+ srv.createdAt.difference(pending.createdAt).inSeconds.abs() < 60,
+ orElse: () => null,
+ );
}
- // handle for receivers
- void handleIncomingMessage(Map data) async {
- if (_activeChatId == null) return;
+ Future sendMessage({
+ required String content,
+ String messageType = 'text',
+ String? mediaUrl,
+ }) async {
+ if (_activeChatId == null || _myUserId == null) return;
+
+ final tempId = 'temp_${const Uuid().v4()}';
+ final now = DateTime.now();
+
+ final localMessage = MessageModel(
+ id: tempId,
+ chatId: _activeChatId!,
+ userId: _myUserId!,
+ content: content,
+ messageType: messageType,
+ createdAt: now,
+ status: 'SENDING',
+ );
- final msg = MessageModel.fromApiResponse(data);
- if (msg.userId == _myUserId) {
- return;
- }
+ await _chatLocalRepository.saveMessage(localMessage);
- final exists = state.messages.any((m) => m.id == msg.id);
- if (exists) {
- _updateLocalMessageWithServerId(msg);
- return;
- }
+ final updatedMessages = [...state.messages, localMessage];
+ state = state.copyWith(messages: updatedMessages);
+ _loadedMessageIds.add(tempId);
- final isActiveChat =
- (msg.userId != _myUserId && msg.chatId == _activeChatId);
+ ref
+ .read(conversationsViewModelProvider.notifier)
+ .updateConversationAfterSending(
+ chatId: _activeChatId!,
+ content: content,
+ messageType: messageType,
+ );
- if (isActiveChat) {
- msg.status = "READ";
- }
+ _socketRepository.sendMessage(localMessage.toApiRequest());
+ }
- await _chatLocalRepository.saveMessage(msg);
+ void _handleMessageAck(Map data) async {
+ final chatId = data["chatId"] as String?;
+ final realMessageId = data["messageId"] as String?;
- final updated = [...state.messages, msg];
- state = state.copyWith(messages: updated);
- if (isActiveChat) {
- _socketRepository.openChat(_activeChatId!);
- }
- }
+ if (chatId == null || realMessageId == null) return;
+ if (_activeChatId != chatId) return;
- void _updateLocalMessageWithServerId(MessageModel serverMsg) {
- final index = state.messages.lastIndexWhere(
- (m) => m.status == "PENDING" && m.chatId == serverMsg.chatId,
+ final index = state.messages.indexWhere(
+ (m) => m.status == "SENDING" && m.chatId == chatId,
);
if (index == -1) return;
final tempMsg = state.messages[index];
- final updatedMsg = tempMsg.copyWith(
- id: serverMsg.id,
- status: serverMsg.status,
- );
+ final serverMessage = tempMsg.copyWith(id: realMessageId, status: "SENT");
- final updated = [...state.messages];
- updated[index] = updatedMsg;
+ await _chatLocalRepository.replaceTempWithServerMessage(
+ tempId: tempMsg.id,
+ serverMessage: serverMessage,
+ );
- _chatLocalRepository.saveMessage(updatedMsg);
+ _loadedMessageIds.remove(tempMsg.id);
+ _loadedMessageIds.add(realMessageId);
- state = state.copyWith(messages: updated);
+ final updatedMessages = [...state.messages];
+ updatedMessages[index] = serverMessage;
+ state = state.copyWith(messages: updatedMessages);
}
- // send message
- Future sendMessage(MessageModel localMessage) async {
- _chatLocalRepository.saveMessage(localMessage);
+ void _handleIncomingMessage(Map data) async {
+ final msg = MessageModel.fromApiResponse(data);
+
+ if (msg.userId == _myUserId) return;
+ if (_loadedMessageIds.contains(msg.id)) return;
- final isAlreadyInState = state.messages.any((m) => m.id == localMessage.id);
+ final isActiveChat = (_activeChatId == msg.chatId);
+ final finalMsg = msg.copyWith(status: isActiveChat ? "READ" : msg.status);
- if (!isAlreadyInState) {
- final updated = [...state.messages, localMessage];
- state = state.copyWith(messages: updated);
+ await _chatLocalRepository.saveMessage(finalMsg);
+
+ if (isActiveChat) {
+ final updatedMessages = [...state.messages, finalMsg];
+ state = state.copyWith(messages: updatedMessages);
+ _loadedMessageIds.add(msg.id);
+ _socketRepository.openChat(_activeChatId!);
}
- ref
- .read(conversationsViewModelProvider.notifier)
- .updateConversationAfterSending(
- chatId: localMessage.chatId,
- content: localMessage.content ?? '',
- messageType: localMessage.messageType,
- );
- _socketRepository.sendMessage(localMessage.toApiRequest());
}
- // handle read status event
- void handleMessagesRead(Map data) {
- final chatId = data['chatId'];
+ void _handleMessagesRead(Map data) async {
+ final chatId = data['chatId'] as String?;
if (chatId == null || chatId != _activeChatId) return;
- _chatLocalRepository.markMessagesAsRead(chatId, _myUserId!);
- final updated = _chatLocalRepository.getMessagesForChat(chatId);
- state = state.copyWith(messages: updated);
+
+ final messagesBeforeUpdate = [...state.messages];
+
+ await _chatLocalRepository.markMessagesAsRead(chatId, _myUserId!);
+
+ final updatedMessages = messagesBeforeUpdate.map((msg) {
+ if (msg.chatId == chatId &&
+ msg.userId == _myUserId &&
+ msg.status != "READ") {
+ return msg.copyWith(status: "READ");
+ }
+ return msg;
+ }).toList();
+
+ state = state.copyWith(messages: updatedMessages);
+ }
+
+ Future loadOlderMessages() async {
+ if (_activeChatId == null ||
+ state.isLoadingHistory ||
+ !state.hasMoreHistory) {
+ return;
+ }
+
+ final allCurrentMessages = [...state.messages, ..._historyBuffer];
+ if (allCurrentMessages.isEmpty) return;
+
+ final sortedCurrent = _sortMessages(allCurrentMessages);
+ final oldestMessage = sortedCurrent.first;
+ final lastTimestamp = oldestMessage.createdAt;
+
+ state = state.copyWith(isLoadingHistory: true);
+
+ final result = await _chatRemoteRepository.getOlderMessagesChat(
+ chatId: _activeChatId!,
+ lastMessageTimestamp: lastTimestamp,
+ );
+
+ await result.fold(
+ (failure) async {
+ state = state.copyWith(isLoadingHistory: false);
+ },
+ (olderMessages) async {
+ if (olderMessages.isEmpty) {
+ state = state.copyWith(
+ isLoadingHistory: false,
+ hasMoreHistory: false,
+ );
+ return;
+ }
+
+ final newMessages = olderMessages
+ .where((msg) => !_loadedMessageIds.contains(msg.id))
+ .toList();
+
+ _historyBuffer.addAll(newMessages);
+ _loadedMessageIds.addAll(newMessages.map((m) => m.id));
+
+ final allMessages = [..._historyBuffer, ...state.messages];
+
+ state = state.copyWith(messages: allMessages, isLoadingHistory: false);
+ },
+ );
}
- // typing indicator
void sendTyping(bool isTyping) {
if (_activeChatId == null) return;
_socketRepository.sendTyping(_activeChatId!, isTyping);
}
- void handleTypingEvent(dynamic data) {
+ void _handleTypingEvent(dynamic data) {
if (_activeChatId == null) return;
final typingChatId = data['chatId'] as String?;
@@ -300,15 +349,12 @@ class ChatViewModel extends _$ChatViewModel {
}
void exitChat() {
- final previousChatId = _activeChatId;
+ if (_activeChatId != null) {
+ _socketRepository.leaveChat(_activeChatId!);
+ } // will be handled in backend
_activeChatId = null;
-
- if (previousChatId != null) {
- state = ChatState(
- isRecipientTyping: false,
- messages: [],
- isLoading: false,
- );
- }
+ _historyBuffer.clear();
+ _loadedMessageIds.clear();
+ state = ChatState(isRecipientTyping: false, messages: [], isLoading: false);
}
}
diff --git a/lib/features/chat/view_model/conversions/Conversations_view_model.dart b/lib/features/chat/view_model/conversions/Conversations_view_model.dart
index e76d8ea..582cc6c 100644
--- a/lib/features/chat/view_model/conversions/Conversations_view_model.dart
+++ b/lib/features/chat/view_model/conversions/Conversations_view_model.dart
@@ -41,6 +41,7 @@ class ConversationsViewModel extends _$ConversationsViewModel {
void _listenToNewMessages() {
_socketRepository.onNewMessage((data) {
try {
+ print("New message received: $data");
final newMsg = MessageModel.fromApiResponse(data);
final currentConversations = state.maybeWhen(
@@ -50,11 +51,10 @@ class ConversationsViewModel extends _$ConversationsViewModel {
final idx = currentConversations.indexWhere(
(chat) => chat.id == newMsg.chatId,
- ); // check if conversation already exist or not
+ );
final openChatId = ref.read(activeChatProvider);
final bool isChatOpen = openChatId == newMsg.chatId;
if (idx != -1) {
- // conversation exists
final chat = currentConversations[idx];
final bool isMe = newMsg.userId == _currentUser?.id;
final int newCount;
@@ -75,7 +75,6 @@ class ConversationsViewModel extends _$ConversationsViewModel {
currentConversations[idx] = updatedChat;
} else {
- //handle dm only
final bool isMe = newMsg.userId == _currentUser?.id;
final int initialUnseenCount = (isMe || isChatOpen) ? 0 : 1;
final created = ConversationModel(
@@ -165,7 +164,6 @@ class ConversationsViewModel extends _$ConversationsViewModel {
lastMessageType: messageType,
lastMessageTime: DateTime.now(),
lastMessageSenderId: _currentUser!.id,
- unseenCount: 0,
);
final updatedList = List.from(currentList);
From 2b49953d0f74f81ef06a00cddd60162613260ecb Mon Sep 17 00:00:00 2001
From: Hazem-Emam-404
Date: Thu, 4 Dec 2025 13:13:12 +0200
Subject: [PATCH 05/30] Update upload_media.dart
---
lib/features/media/upload_media.dart | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/lib/features/media/upload_media.dart b/lib/features/media/upload_media.dart
index 9d9b64a..b2257cd 100644
--- a/lib/features/media/upload_media.dart
+++ b/lib/features/media/upload_media.dart
@@ -17,7 +17,7 @@ Future> upload_media(List files) async {
final fileType = _getMediaType(file.path);
// request upload
-
+
final requestUpload = container.read(requestUploadProvider);
final requestUploadResponse = await requestUpload(fileName, fileType);
RequestUploadModel requestUploadModel = RequestUploadModel(
@@ -85,6 +85,18 @@ const Map _mediaTypes = {
'png': 'image/png',
'gif': 'image/gif',
'webp': 'image/webp',
+
+ 'mp4': 'video/mp4',
+ 'mov': 'video/quicktime',
+ 'avi': 'video/x-msvideo',
+ 'webm': 'video/webm',
+ 'mkv': 'video/x-matroska',
+ 'flv': 'video/x-flv',
+ 'wmv': 'video/x-ms-wmv',
+ 'mpeg': 'video/mpeg',
+ 'mpg': 'video/mpeg',
+ '3gp': 'video/3gpp',
+ 'm4v': 'video/x-m4v',
};
String _getMediaType(String filePath) {
final extension = filePath.split('.').last.toLowerCase();
From 2d64da50a76a50f2593e2ec7f6b2e2e8132fd019 Mon Sep 17 00:00:00 2001
From: Hazem-Emam-404
Date: Thu, 4 Dec 2025 13:21:16 +0200
Subject: [PATCH 06/30] update media
---
lib/features/media/models/shared.dart | 23 ++++++++++++++++
.../media/repository/media_repo_impl.dart | 19 +++-----------
lib/features/media/upload_media.dart | 26 ++-----------------
3 files changed, 28 insertions(+), 40 deletions(-)
create mode 100644 lib/features/media/models/shared.dart
diff --git a/lib/features/media/models/shared.dart b/lib/features/media/models/shared.dart
new file mode 100644
index 0000000..57b0ca9
--- /dev/null
+++ b/lib/features/media/models/shared.dart
@@ -0,0 +1,23 @@
+const Map _mediaTypes = {
+ 'jpg': 'image/jpeg',
+ 'jpeg': 'image/jpeg',
+ 'png': 'image/png',
+ 'gif': 'image/gif',
+ 'webp': 'image/webp',
+
+ 'mp4': 'video/mp4',
+ 'mov': 'video/quicktime',
+ 'avi': 'video/x-msvideo',
+ 'webm': 'video/webm',
+ 'mkv': 'video/x-matroska',
+ 'flv': 'video/x-flv',
+ 'wmv': 'video/x-ms-wmv',
+ 'mpeg': 'video/mpeg',
+ 'mpg': 'video/mpeg',
+ '3gp': 'video/3gpp',
+ 'm4v': 'video/x-m4v',
+};
+String getMediaType(String filePath) {
+ final extension = filePath.split('.').last.toLowerCase();
+ return _mediaTypes[extension] ?? 'image/jpeg';
+}
diff --git a/lib/features/media/repository/media_repo_impl.dart b/lib/features/media/repository/media_repo_impl.dart
index 253ef5f..f6c680c 100644
--- a/lib/features/media/repository/media_repo_impl.dart
+++ b/lib/features/media/repository/media_repo_impl.dart
@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart';
+import 'package:lite_x/features/media/models/shared.dart';
import 'package:lite_x/features/profile/models/shared.dart';
import 'package:lite_x/features/media/models/confirm_upload_model.dart';
import 'package:lite_x/features/media/models/request_upload_model.dart';
@@ -9,9 +10,7 @@ import 'package:lite_x/features/media/repository/media_repo.dart';
class MediaRepoImpL implements MediaRepo {
Dio _dio;
- MediaRepoImpL(Dio d) : _dio = d {
-
- }
+ MediaRepoImpL(Dio d) : _dio = d {}
Future> requestUpload(
String fileName,
@@ -51,7 +50,7 @@ class MediaRepoImpL implements MediaRepo {
data: Stream.fromIterable([fileBytes]),
options: Options(
headers: {
- 'Content-Type': _getMediaType(mediaFile.path),
+ 'Content-Type': getMediaType(mediaFile.path),
'Content-Length': fileBytes.length,
},
),
@@ -77,18 +76,6 @@ class MediaRepoImpL implements MediaRepo {
}
}
-const Map _mediaTypes = {
- 'jpg': 'image/jpg',
- 'jpeg': 'image/jpeg',
- 'png': 'image/png',
- 'gif': 'image/gif',
- 'webp': 'image/webp',
-};
-String _getMediaType(String filePath) {
- final extension = filePath.split('.').last.toLowerCase();
- return _mediaTypes[extension] ?? 'image/jpeg';
-}
-
// Future>> uploadProfilePhoto({
// required PickedImage pickedImage,
// }) async {
diff --git a/lib/features/media/upload_media.dart b/lib/features/media/upload_media.dart
index b2257cd..4637eed 100644
--- a/lib/features/media/upload_media.dart
+++ b/lib/features/media/upload_media.dart
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lite_x/features/media/models/confirm_upload_model.dart';
import 'package:lite_x/features/media/models/request_upload_model.dart';
+import 'package:lite_x/features/media/models/shared.dart';
import 'package:lite_x/features/media/view_model/providers.dart';
Future> upload_media(List files) async {
@@ -14,7 +15,7 @@ Future> upload_media(List files) async {
final file = limitedFiles[i];
bool fail = false;
final fileName = file.path.split(Platform.pathSeparator).last;
- final fileType = _getMediaType(file.path);
+ final fileType = getMediaType(file.path);
// request upload
@@ -79,26 +80,3 @@ Future> upload_media(List files) async {
return ids;
}
-const Map _mediaTypes = {
- 'jpg': 'image/jpeg',
- 'jpeg': 'image/jpeg',
- 'png': 'image/png',
- 'gif': 'image/gif',
- 'webp': 'image/webp',
-
- 'mp4': 'video/mp4',
- 'mov': 'video/quicktime',
- 'avi': 'video/x-msvideo',
- 'webm': 'video/webm',
- 'mkv': 'video/x-matroska',
- 'flv': 'video/x-flv',
- 'wmv': 'video/x-ms-wmv',
- 'mpeg': 'video/mpeg',
- 'mpg': 'video/mpeg',
- '3gp': 'video/3gpp',
- 'm4v': 'video/x-m4v',
-};
-String _getMediaType(String filePath) {
- final extension = filePath.split('.').last.toLowerCase();
- return _mediaTypes[extension] ?? 'image/jpeg';
-}
From cbe240009181e4b51649a4bc2b731fc1d2f31078 Mon Sep 17 00:00:00 2001
From: yaraFarouk
Date: Thu, 4 Dec 2025 13:26:36 +0200
Subject: [PATCH 07/30] some edits in tweet
---
lib/features/home/models/tweet_model.dart | 7 +-
.../home/repositories/home_repository.dart | 26 +-
.../home/repositories/mock_home_data.dart | 60 +++
.../home/view/screens/home_screen.dart | 112 +++---
.../view/screens/reply_composer_screen.dart | 4 +-
.../view/screens/reply_thread_screen.dart | 195 ++++++++--
.../home/view/screens/tweet_screen.dart | 223 +++++++++--
.../home/view/widgets/home_tab_bar.dart | 1 +
.../view/widgets/tweet_summary_dialog.dart | 350 ++++++++++++------
lib/features/home/view_model/home_state.dart | 2 +-
.../home/view_model/home_view_model.dart | 2 +-
11 files changed, 739 insertions(+), 243 deletions(-)
create mode 100644 lib/features/home/repositories/mock_home_data.dart
diff --git a/lib/features/home/models/tweet_model.dart b/lib/features/home/models/tweet_model.dart
index dad9104..10d3711 100644
--- a/lib/features/home/models/tweet_model.dart
+++ b/lib/features/home/models/tweet_model.dart
@@ -178,14 +178,19 @@ class TweetModel extends HiveObject {
authorName:
user?['name']?.toString() ??
+ json['name']?.toString() ??
json['authorName']?.toString() ??
'Unknown User',
authorUsername:
user?['username']?.toString() ??
+ json['username']?.toString() ??
json['authorUsername']?.toString() ??
'unknown',
authorAvatar:
- _extractProfileMedia(user) ?? json['authorAvatar']?.toString() ?? '',
+ _extractProfileMedia(user) ??
+ json['profileMediaKey']?.toString() ??
+ json['authorAvatar']?.toString() ??
+ '',
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'])
: DateTime.now(),
diff --git a/lib/features/home/repositories/home_repository.dart b/lib/features/home/repositories/home_repository.dart
index 40048ff..c259815 100644
--- a/lib/features/home/repositories/home_repository.dart
+++ b/lib/features/home/repositories/home_repository.dart
@@ -28,10 +28,17 @@ class HomeRepository {
);
final List tweetsData;
- if (response.data is List) {
+ // Handle response format: { "user": {...}, "recommendations": [...] }
+ if (response.data is Map) {
+ if (response.data['recommendations'] != null) {
+ tweetsData = response.data['recommendations'] as List;
+ } else if (response.data['data'] != null) {
+ tweetsData = response.data['data'] as List;
+ } else {
+ return [];
+ }
+ } else if (response.data is List) {
tweetsData = response.data as List;
- } else if (response.data is Map && response.data['data'] != null) {
- tweetsData = response.data['data'] as List;
} else {
return [];
}
@@ -53,6 +60,8 @@ class HomeRepository {
int page = 1,
int limit = 20,
}) async {
+ // Return mock data if enabled
+
try {
final response = await _dio.get(
'api/home/timeline',
@@ -60,10 +69,15 @@ class HomeRepository {
);
final List tweetsData;
- if (response.data is List) {
+ // Handle response format: { "data": [...], "nextCursor": "..." }
+ if (response.data is Map) {
+ if (response.data['data'] != null) {
+ tweetsData = response.data['data'] as List;
+ } else {
+ return [];
+ }
+ } else if (response.data is List) {
tweetsData = response.data as List;
- } else if (response.data is Map && response.data['data'] != null) {
- tweetsData = response.data['data'] as List;
} else {
return [];
}
diff --git a/lib/features/home/repositories/mock_home_data.dart b/lib/features/home/repositories/mock_home_data.dart
new file mode 100644
index 0000000..ed9a25a
--- /dev/null
+++ b/lib/features/home/repositories/mock_home_data.dart
@@ -0,0 +1,60 @@
+import 'package:lite_x/features/home/models/tweet_model.dart';
+
+/// Mock data for testing when endpoints are offline
+class MockHomeData {
+ static const bool useMockData = false; // Set to true to use mock data
+
+ static List getMockForYouTweets() {
+ final now = DateTime.now();
+
+ return [
+ TweetModel(
+ id: 'mock-foryou-1',
+ content: ' Welcome to the For You feed! This is a mock tweet for testing purposes.',
+ authorName: 'Test User',
+ authorUsername: 'testuser',
+ authorAvatar: '',
+ userId: 'user-1',
+ createdAt: now.subtract(const Duration(hours: 2)),
+ likes: 42,
+ retweets: 15,
+ replies: 8,
+ quotes: 3,
+ bookmarks: 5,
+ isLiked: false,
+ isRetweeted: false,
+ isBookmarked: false,
+ tweetType: 'TWEET',
+ images: [],
+ replyIds: [],
+ ),
+ ];
+ }
+
+ static List getMockTimelineTweets() {
+ final now = DateTime.now();
+
+ return [
+ TweetModel(
+ id: 'mock-timeline-1',
+ content: ' This is your Following/Timeline feed! Here you see tweets from people you follow.',
+ authorName: 'Your Friend',
+ authorUsername: 'yourfriend',
+ authorAvatar: '',
+ userId: 'user-10',
+ createdAt: now.subtract(const Duration(minutes: 30)),
+ likes: 25,
+ retweets: 8,
+ replies: 4,
+ quotes: 1,
+ bookmarks: 2,
+ isLiked: false,
+ isRetweeted: false,
+ isBookmarked: false,
+ tweetType: 'TWEET',
+ images: [],
+ replyIds: [],
+ ),
+ ];
+ }
+}
diff --git a/lib/features/home/view/screens/home_screen.dart b/lib/features/home/view/screens/home_screen.dart
index 3e8a3d8..ba84b2e 100644
--- a/lib/features/home/view/screens/home_screen.dart
+++ b/lib/features/home/view/screens/home_screen.dart
@@ -13,10 +13,7 @@ import 'package:lite_x/features/home/view/screens/create_post_screen.dart';
import 'package:lite_x/features/home/view/screens/quote_composer_screen.dart';
import 'package:lite_x/features/home/view/widgets/profile_side_drawer.dart';
import 'package:lite_x/features/home/view/widgets/expandable_fab.dart';
-import 'package:lite_x/features/profile/models/shared.dart';
import 'package:lite_x/features/profile/view/screens/profile_screen.dart';
-import 'package:lite_x/features/profile/view/widgets/profile_tweets/profile_posts_list.dart';
-import 'package:lite_x/features/profile/view_model/providers.dart';
class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({super.key});
@@ -114,22 +111,17 @@ class _HomeScreenState extends ConsumerState
super.build(context);
final homeState = ref.watch(homeViewModelProvider);
final currentFeed = homeState.currentFeed;
- final tweets = currentFeed == FeedType.forYou
- ? homeState.forYouTweets
- : homeState.followingTweets;
+ final tweets =
+ homeState.tweets; // Use the tweets field that switches correctly
final feedName = currentFeed == FeedType.forYou ? "For You" : "Following";
- final currentUserName = ref.watch(currentUserProvider)?.username ?? "";
- final profileData = ref.watch(profileDataProvider(currentUserName));
return Scaffold(
key: _scaffoldKey,
backgroundColor: Colors.black,
drawer: const ProfileSideDrawer(),
body: RefreshIndicator(
onRefresh: () async {
- ref.read(homeViewModelProvider.notifier).refreshTweets();
- // ignore: unused_result
- await ref.refresh(profilePostsProvider(currentUserName));
+ await ref.read(homeViewModelProvider.notifier).refreshTweets();
},
backgroundColor: Colors.grey[900],
color: Colors.white,
@@ -152,57 +144,11 @@ class _HomeScreenState extends ConsumerState
collapseMode: CollapseMode.pin,
),
),
-
- // _buildSliverTweetList(
- // context,
- // tweets,
- // homeState.isLoading,
- // feedName,
- // ),
- SliverFillRemaining(
- child: profileData.when(
- data: (res) {
- return res.fold(
- (l) {
- return RefreshIndicator(
- onRefresh: () async {
- // ignore: unused_result
- await ref.refresh(
- profileDataProvider(currentUserName),
- );
- },
- child: ListView(
- children: [Center(child: Text(l.message))],
- ),
- );
- },
- (data) {
- return ProfilePostsList(
- profile: data,
- tabType: ProfileTabType.Posts,
- );
- },
- );
- },
- error: (err, _) {
- return RefreshIndicator(
- onRefresh: () async {
- // ignore: unused_result
- await ref.refresh(profileDataProvider(currentUserName));
- },
- child: ListView(
- children: [
- Center(child: Text("Can't get profile posts")),
- ],
- ),
- );
- },
- loading: () {
- return ListView(
- children: [Center(child: CircularProgressIndicator())],
- );
- },
- ),
+ _buildSliverTweetList(
+ context,
+ tweets,
+ homeState.isLoading,
+ feedName,
),
],
),
@@ -257,6 +203,48 @@ class _HomeScreenState extends ConsumerState
bool isLoading,
String feedType,
) {
+ // Show error if present
+ final homeState = ref.watch(homeViewModelProvider);
+ if (homeState.error != null && tweets.isEmpty) {
+ return SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.only(top: 50.0),
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.error_outline, size: 64, color: Colors.red[400]),
+ const SizedBox(height: 16),
+ Text(
+ 'Error loading tweets',
+ style: TextStyle(color: Colors.grey[400], fontSize: 18),
+ ),
+ const SizedBox(height: 8),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32.0),
+ child: Text(
+ homeState.error!,
+ textAlign: TextAlign.center,
+ style: TextStyle(color: Colors.grey[500], fontSize: 14),
+ ),
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton(
+ onPressed: () {
+ ref.read(homeViewModelProvider.notifier).refreshTweets();
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: const Color(0xFF1DA1F2),
+ ),
+ child: const Text('Retry'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
if (isLoading && tweets.isEmpty) {
return const SliverToBoxAdapter(
child: Padding(
diff --git a/lib/features/home/view/screens/reply_composer_screen.dart b/lib/features/home/view/screens/reply_composer_screen.dart
index 5af6a6e..7d1a71b 100644
--- a/lib/features/home/view/screens/reply_composer_screen.dart
+++ b/lib/features/home/view/screens/reply_composer_screen.dart
@@ -361,7 +361,7 @@ class _ReplyComposerScreenState extends ConsumerState {
const SizedBox(width: 4),
Flexible(
child: Text(
- widget.replyingToTweet.authorUsername,
+ '@${widget.replyingToTweet.authorUsername}',
style: TextStyle(color: Colors.grey[500], fontSize: 15),
overflow: TextOverflow.ellipsis,
),
@@ -386,7 +386,7 @@ class _ReplyComposerScreenState extends ConsumerState {
children: [
const TextSpan(text: 'Replying to '),
TextSpan(
- text: widget.replyingToTweet.authorUsername,
+ text: '@${widget.replyingToTweet.authorUsername}',
style: const TextStyle(color: Color(0xFF1D9BF0)),
),
],
diff --git a/lib/features/home/view/screens/reply_thread_screen.dart b/lib/features/home/view/screens/reply_thread_screen.dart
index 6383c72..b016ba7 100644
--- a/lib/features/home/view/screens/reply_thread_screen.dart
+++ b/lib/features/home/view/screens/reply_thread_screen.dart
@@ -6,6 +6,7 @@ import 'package:lite_x/features/home/models/tweet_model.dart';
import 'package:lite_x/features/home/repositories/home_repository.dart';
import 'package:lite_x/features/home/view/screens/reply_composer_screen.dart';
import 'package:lite_x/features/home/view/widgets/media_gallery.dart';
+import 'package:lite_x/features/profile/view_model/providers.dart';
import 'package:timeago/timeago.dart' as timeago;
class ReplyThreadScreen extends ConsumerStatefulWidget {
@@ -26,6 +27,8 @@ class _ReplyThreadScreenState extends ConsumerState {
bool isLoading = true;
String? currentUserId;
final Map _viewCounts = {};
+ bool isFollowing = false;
+ bool isFollowLoading = false;
@override
void initState() {
@@ -42,6 +45,77 @@ class _ReplyThreadScreenState extends ConsumerState {
}
}
+ Future _toggleFollow() async {
+ final currentReplyTweet = allTweets[widget.pathTweetIds.last];
+ if (currentReplyTweet == null || isFollowLoading) return;
+
+ setState(() {
+ isFollowLoading = true;
+ });
+
+ try {
+ final username = currentReplyTweet.authorUsername;
+
+ if (isFollowing) {
+ final unfollowFunc = ref.read(unFollowControllerProvider);
+ final result = await unfollowFunc(username);
+ result.fold(
+ (failure) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Failed to unfollow: ${failure.message}'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ },
+ (_) {
+ if (mounted) {
+ setState(() {
+ isFollowing = false;
+ });
+ }
+ },
+ );
+ } else {
+ final followFunc = ref.read(followControllerProvider);
+ final result = await followFunc(username);
+ result.fold(
+ (failure) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Failed to follow: ${failure.message}'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ },
+ (_) {
+ if (mounted) {
+ setState(() {
+ isFollowing = true;
+ });
+ }
+ },
+ );
+ }
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
+ );
+ }
+ } finally {
+ if (mounted) {
+ setState(() {
+ isFollowLoading = false;
+ });
+ }
+ }
+ }
+
Future _toggleLike(String tweetId) async {
// Find tweet in allTweets or childReplies
TweetModel? tweet = allTweets[tweetId];
@@ -396,19 +470,26 @@ class _ReplyThreadScreenState extends ConsumerState {
),
),
const SizedBox(width: 6),
- Text(
- tweet.authorUsername,
- style: TextStyle(
- color: Colors.grey[600],
- fontSize: 15,
+ Flexible(
+ child: Text(
+ '@${tweet.authorUsername}',
+ style: TextStyle(
+ color: Colors.grey[600],
+ fontSize: 15,
+ ),
+ overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 6),
- Text(
- '· ${timeago.format(tweet.createdAt, locale: 'en_short')}',
- style: TextStyle(
- color: Colors.grey[600],
- fontSize: 15,
+ Flexible(
+ fit: FlexFit.loose,
+ child: Text(
+ '· ${timeago.format(tweet.createdAt, locale: 'en_short')}',
+ style: TextStyle(
+ color: Colors.grey[600],
+ fontSize: 15,
+ ),
+ overflow: TextOverflow.ellipsis,
),
),
],
@@ -486,14 +567,27 @@ class _ReplyThreadScreenState extends ConsumerState {
),
),
const SizedBox(width: 4),
- Text(
- '@${reply.authorUsername}',
- style: TextStyle(color: Colors.grey[600], fontSize: 15),
+ Flexible(
+ child: Text(
+ '@${reply.authorUsername}',
+ style: TextStyle(
+ color: Colors.grey[600],
+ fontSize: 15,
+ ),
+ overflow: TextOverflow.ellipsis,
+ ),
),
const SizedBox(width: 4),
- Text(
- '· ${timeago.format(reply.createdAt, locale: 'en_short')}',
- style: TextStyle(color: Colors.grey[600], fontSize: 15),
+ Flexible(
+ fit: FlexFit.loose,
+ child: Text(
+ '· ${timeago.format(reply.createdAt, locale: 'en_short')}',
+ style: TextStyle(
+ color: Colors.grey[600],
+ fontSize: 15,
+ ),
+ overflow: TextOverflow.ellipsis,
+ ),
),
],
),
@@ -504,11 +598,14 @@ class _ReplyThreadScreenState extends ConsumerState {
'Replying to ',
style: TextStyle(color: Colors.grey[600], fontSize: 15),
),
- Text(
- replyingTo,
- style: const TextStyle(
- color: Colors.blue,
- fontSize: 15,
+ Flexible(
+ child: Text(
+ '@$replyingTo',
+ style: const TextStyle(
+ color: Colors.blue,
+ fontSize: 15,
+ ),
+ overflow: TextOverflow.ellipsis,
),
),
],
@@ -591,9 +688,12 @@ class _ReplyThreadScreenState extends ConsumerState {
'Replying to ',
style: TextStyle(color: Colors.grey[600], fontSize: 15),
),
- Text(
- replyingTo,
- style: const TextStyle(color: Colors.blue, fontSize: 15),
+ Flexible(
+ child: Text(
+ '@$replyingTo',
+ style: const TextStyle(color: Colors.blue, fontSize: 15),
+ overflow: TextOverflow.ellipsis,
+ ),
),
],
),
@@ -701,19 +801,42 @@ class _ReplyThreadScreenState extends ConsumerState {
} else {
return Row(
children: [
- Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(20),
- ),
- child: const Text(
- 'Follow',
- style: TextStyle(
- color: Colors.black,
- fontWeight: FontWeight.bold,
- fontSize: 15,
+ GestureDetector(
+ onTap: isFollowLoading ? null : _toggleFollow,
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+ decoration: BoxDecoration(
+ color: isFollowing ? Colors.transparent : Colors.white,
+ border: isFollowing
+ ? Border.all(color: Colors.grey[700]!, width: 1)
+ : null,
+ borderRadius: BorderRadius.circular(20),
),
+ child: isFollowLoading
+ ? const SizedBox(
+ width: 60,
+ height: 20,
+ child: Center(
+ child: SizedBox(
+ width: 16,
+ height: 16,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ valueColor: AlwaysStoppedAnimation(
+ Colors.grey,
+ ),
+ ),
+ ),
+ ),
+ )
+ : Text(
+ isFollowing ? 'Following' : 'Follow',
+ style: TextStyle(
+ color: isFollowing ? Colors.white : Colors.black,
+ fontWeight: FontWeight.bold,
+ fontSize: 15,
+ ),
+ ),
),
),
const SizedBox(width: 6),
diff --git a/lib/features/home/view/screens/tweet_screen.dart b/lib/features/home/view/screens/tweet_screen.dart
index 3b08680..0b9603c 100644
--- a/lib/features/home/view/screens/tweet_screen.dart
+++ b/lib/features/home/view/screens/tweet_screen.dart
@@ -2,6 +2,7 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:intl/intl.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
import 'package:lite_x/features/home/models/tweet_model.dart';
@@ -12,6 +13,7 @@ import 'package:lite_x/features/home/view/screens/quote_composer_screen.dart';
import 'package:lite_x/features/home/view/widgets/media_gallery.dart';
import 'package:lite_x/features/home/view/widgets/tweet_summary_dialog.dart';
import 'package:lite_x/features/profile/view/screens/profile_screen.dart';
+import 'package:lite_x/features/profile/view_model/providers.dart';
import 'package:lite_x/features/home/view_model/home_view_model.dart';
import 'package:timeago/timeago.dart' as timeago;
@@ -30,6 +32,8 @@ class _TweetDetailScreenState extends ConsumerState {
bool isLoading = true;
String? currentUserId;
int? _viewCount;
+ bool isFollowing = false;
+ bool isFollowLoading = false;
@override
void initState() {
@@ -58,6 +62,76 @@ class _TweetDetailScreenState extends ConsumerState {
return username.startsWith('@') ? username.substring(1) : username;
}
+ Future _toggleFollow() async {
+ if (mainTweet == null || isFollowLoading) return;
+
+ setState(() {
+ isFollowLoading = true;
+ });
+
+ try {
+ final username = mainTweet!.authorUsername;
+
+ if (isFollowing) {
+ final unfollowFunc = ref.read(unFollowControllerProvider);
+ final result = await unfollowFunc(username);
+ result.fold(
+ (failure) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Failed to unfollow: ${failure.message}'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ },
+ (_) {
+ if (mounted) {
+ setState(() {
+ isFollowing = false;
+ });
+ }
+ },
+ );
+ } else {
+ final followFunc = ref.read(followControllerProvider);
+ final result = await followFunc(username);
+ result.fold(
+ (failure) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Failed to follow: ${failure.message}'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ },
+ (_) {
+ if (mounted) {
+ setState(() {
+ isFollowing = true;
+ });
+ }
+ },
+ );
+ }
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
+ );
+ }
+ } finally {
+ if (mounted) {
+ setState(() {
+ isFollowLoading = false;
+ });
+ }
+ }
+ }
+
Future _loadTweetData() async {
final cachedTweet = _findCachedTweet(widget.tweetId);
@@ -304,11 +378,48 @@ class _TweetDetailScreenState extends ConsumerState {
Future _showSummaryDialog() async {
if (mainTweet == null) return;
+ // Show loading dialog
+ showDialog(
+ context: context,
+ barrierDismissible: false,
+ builder: (context) => Dialog(
+ backgroundColor: Colors.transparent,
+ child: Container(
+ padding: const EdgeInsets.all(24),
+ decoration: BoxDecoration(
+ color: Colors.black,
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(
+ color: const Color(0xFF1DA1F2).withOpacity(0.3),
+ width: 1,
+ ),
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const CircularProgressIndicator(
+ valueColor: AlwaysStoppedAnimation(Color(0xFF1DA1F2)),
+ ),
+ const SizedBox(height: 16),
+ Text(
+ 'Generating AI insights...',
+ style: TextStyle(color: Colors.grey[400], fontSize: 14),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+
try {
final repository = ref.read(homeRepositoryProvider);
final summary = await repository.getTweetSummary(mainTweet!.id);
if (mounted) {
+ // Close loading dialog
+ Navigator.of(context).pop();
+
+ // Show summary dialog
showDialog(
context: context,
builder: (context) => TweetSummaryDialog(summary: summary),
@@ -316,6 +427,9 @@ class _TweetDetailScreenState extends ConsumerState {
}
} catch (e) {
if (mounted) {
+ // Close loading dialog
+ Navigator.of(context).pop();
+
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to load summary: $e'),
@@ -760,19 +874,40 @@ class _TweetDetailScreenState extends ConsumerState {
],
);
} else {
- return Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(20),
- ),
- child: const Text(
- 'Follow',
- style: TextStyle(
- color: Colors.black,
- fontWeight: FontWeight.bold,
- fontSize: 15,
+ return GestureDetector(
+ onTap: isFollowLoading ? null : _toggleFollow,
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
+ decoration: BoxDecoration(
+ color: isFollowing ? Colors.transparent : Colors.white,
+ border: isFollowing
+ ? Border.all(color: Colors.grey[700]!, width: 1)
+ : null,
+ borderRadius: BorderRadius.circular(20),
),
+ child: isFollowLoading
+ ? const SizedBox(
+ width: 60,
+ height: 20,
+ child: Center(
+ child: SizedBox(
+ width: 16,
+ height: 16,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ valueColor: AlwaysStoppedAnimation(Colors.grey),
+ ),
+ ),
+ ),
+ )
+ : Text(
+ isFollowing ? 'Following' : 'Follow',
+ style: TextStyle(
+ color: isFollowing ? Colors.white : Colors.black,
+ fontWeight: FontWeight.bold,
+ fontSize: 15,
+ ),
+ ),
),
);
}
@@ -1057,14 +1192,14 @@ class _TweetDetailScreenState extends ConsumerState {
textDirection: _textDirectionFor(mainTweet!.content),
),
- if (mainTweet!.quotedTweet != null) ...[
- const SizedBox(height: 16),
- _buildQuotedTweet(mainTweet!.quotedTweet!),
- ],
if (mainTweet!.images.isNotEmpty) ...[
const SizedBox(height: 16),
MediaGallery(urls: mainTweet!.images, borderRadius: 16),
],
+ if (mainTweet!.quotedTweet != null) ...[
+ const SizedBox(height: 16),
+ _buildQuotedTweet(mainTweet!.quotedTweet!),
+ ],
],
);
}
@@ -1258,8 +1393,8 @@ class _TweetDetailScreenState extends ConsumerState {
color: mainTweet!.isLiked ? Colors.pink : Colors.grey[600]!,
onTap: _toggleLike,
),
- _buildIconButton(
- icon: Icons.auto_awesome,
+ _buildSvgIconButton(
+ svgPath: 'assets/svg/grok.svg',
color: const Color(0xFF1DA1F2),
onTap: _showSummaryDialog,
),
@@ -1295,6 +1430,26 @@ class _TweetDetailScreenState extends ConsumerState {
);
}
+ Widget _buildSvgIconButton({
+ required String svgPath,
+ required Color color,
+ required VoidCallback onTap,
+ }) {
+ return InkWell(
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(20),
+ child: Padding(
+ padding: const EdgeInsets.all(8),
+ child: SvgPicture.asset(
+ svgPath,
+ width: 20,
+ height: 20,
+ colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
+ ),
+ ),
+ );
+ }
+
Widget _buildReplyCard(TweetModel reply) {
final profileTap = () => _openProfileFromUsername(reply.authorUsername);
return InkWell(
@@ -1392,14 +1547,21 @@ class _TweetDetailScreenState extends ConsumerState {
),
),
const SizedBox(width: 4),
- Text(
- '@${reply.authorUsername}',
- style: TextStyle(color: Colors.grey[600], fontSize: 15),
+ Flexible(
+ child: Text(
+ '@${reply.authorUsername}',
+ style: TextStyle(color: Colors.grey[600], fontSize: 15),
+ overflow: TextOverflow.ellipsis,
+ ),
),
const SizedBox(width: 4),
- Text(
- '· ${timeago.format(reply.createdAt, locale: 'en_short')}',
- style: TextStyle(color: Colors.grey[600], fontSize: 15),
+ Flexible(
+ fit: FlexFit.loose,
+ child: Text(
+ '· ${timeago.format(reply.createdAt, locale: 'en_short')}',
+ style: TextStyle(color: Colors.grey[600], fontSize: 15),
+ overflow: TextOverflow.ellipsis,
+ ),
),
],
),
@@ -1441,11 +1603,14 @@ class _TweetDetailScreenState extends ConsumerState {
'Replying to ',
style: TextStyle(color: Colors.grey[600], fontSize: 15),
),
- GestureDetector(
- onTap: () => _openProfileFromUsername(mainTweet!.authorUsername),
- child: Text(
- mainTweet!.authorUsername,
- style: const TextStyle(color: Colors.blue, fontSize: 15),
+ Flexible(
+ child: GestureDetector(
+ onTap: () => _openProfileFromUsername(mainTweet!.authorUsername),
+ child: Text(
+ '@${mainTweet!.authorUsername}',
+ style: const TextStyle(color: Colors.blue, fontSize: 15),
+ overflow: TextOverflow.ellipsis,
+ ),
),
),
],
diff --git a/lib/features/home/view/widgets/home_tab_bar.dart b/lib/features/home/view/widgets/home_tab_bar.dart
index 32a83c1..0a4f81a 100644
--- a/lib/features/home/view/widgets/home_tab_bar.dart
+++ b/lib/features/home/view/widgets/home_tab_bar.dart
@@ -27,6 +27,7 @@ class _HomeTabBarState extends ConsumerState
);
final initialFeed = ref.read(homeViewModelProvider).currentFeed;
+ // For You is at position 0 (left), Following is at position 1 (right)
_animationController.value = initialFeed == FeedType.following ? 1.0 : 0.0;
}
diff --git a/lib/features/home/view/widgets/tweet_summary_dialog.dart b/lib/features/home/view/widgets/tweet_summary_dialog.dart
index db24362..b50e804 100644
--- a/lib/features/home/view/widgets/tweet_summary_dialog.dart
+++ b/lib/features/home/view/widgets/tweet_summary_dialog.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import '../../models/tweet_summary.dart';
class TweetSummaryDialog extends StatelessWidget {
@@ -9,113 +10,204 @@ class TweetSummaryDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Dialog(
- backgroundColor: Colors.black,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(20),
- side: const BorderSide(color: Color(0xFF1DA1F2), width: 2),
- ),
- child: Padding(
- padding: const EdgeInsets.all(24.0),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- // Grok AI Header
- Row(
+ backgroundColor: Colors.transparent,
+ child: Container(
+ constraints: const BoxConstraints(maxWidth: 500),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [const Color(0xFF1E1E1E), Colors.black],
+ ),
+ borderRadius: BorderRadius.circular(24),
+ border: Border.all(
+ width: 1.5,
+ color: const Color(0xFF1DA1F2).withOpacity(0.3),
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: const Color(0xFF1DA1F2).withOpacity(0.2),
+ blurRadius: 24,
+ spreadRadius: 0,
+ ),
+ ],
+ ),
+ child: SingleChildScrollView(
+ child: Padding(
+ padding: const EdgeInsets.all(28.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
children: [
- Container(
- padding: const EdgeInsets.all(8),
- decoration: BoxDecoration(
- color: const Color(0xFF1DA1F2).withOpacity(0.2),
- borderRadius: BorderRadius.circular(12),
- ),
- child: const Icon(
- Icons.auto_awesome,
- color: Color(0xFF1DA1F2),
- size: 24,
- ),
- ),
- const SizedBox(width: 12),
- const Text(
- 'Tweet Insights',
- style: TextStyle(
- color: Colors.white,
- fontSize: 20,
- fontWeight: FontWeight.bold,
- ),
+ // Grok AI Header with SVG icon
+ Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.all(10),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ const Color(0xFF1DA1F2).withOpacity(0.3),
+ const Color(0xFF1DA1F2).withOpacity(0.1),
+ ],
+ ),
+ borderRadius: BorderRadius.circular(14),
+ border: Border.all(
+ color: const Color(0xFF1DA1F2).withOpacity(0.4),
+ width: 1,
+ ),
+ ),
+ child: SvgPicture.asset(
+ 'assets/svg/grok.svg',
+ width: 28,
+ height: 28,
+ colorFilter: const ColorFilter.mode(
+ Color(0xFF1DA1F2),
+ BlendMode.srcIn,
+ ),
+ ),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'AI Insights',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 22,
+ fontWeight: FontWeight.bold,
+ letterSpacing: 0.5,
+ ),
+ ),
+ Text(
+ 'Powered by Grok',
+ style: TextStyle(
+ color: Colors.grey[500],
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
),
- ],
- ),
- const SizedBox(height: 24),
+ const SizedBox(height: 28),
- // AI Summary Text (if available)
- if (summary.summary != null && summary.summary!.isNotEmpty) ...[
- Container(
- width: double.infinity,
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: const Color(0xFF1DA1F2).withOpacity(0.1),
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: const Color(0xFF1DA1F2).withOpacity(0.3),
- ),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
+ // AI Summary Text (if available)
+ if (summary.summary != null && summary.summary!.isNotEmpty) ...[
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [
+ const Color(0xFF1DA1F2).withOpacity(0.15),
+ const Color(0xFF1DA1F2).withOpacity(0.05),
+ ],
+ ),
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(
+ color: const Color(0xFF1DA1F2).withOpacity(0.3),
+ width: 1,
+ ),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
- const Icon(
- Icons.auto_awesome,
- color: Color(0xFF1DA1F2),
- size: 16,
+ Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.all(6),
+ decoration: BoxDecoration(
+ color: const Color(0xFF1DA1F2).withOpacity(0.2),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: SvgPicture.asset(
+ 'assets/svg/grok.svg',
+ width: 14,
+ height: 14,
+ colorFilter: const ColorFilter.mode(
+ Color(0xFF1DA1F2),
+ BlendMode.srcIn,
+ ),
+ ),
+ ),
+ const SizedBox(width: 10),
+ Text(
+ 'AI Summary',
+ style: TextStyle(
+ color: const Color(0xFF1DA1F2),
+ fontSize: 13,
+ fontWeight: FontWeight.bold,
+ letterSpacing: 0.5,
+ ),
+ ),
+ ],
),
- const SizedBox(width: 8),
+ const SizedBox(height: 16),
Text(
- 'AI Summary',
- style: TextStyle(
- color: Colors.grey[400],
- fontSize: 12,
- fontWeight: FontWeight.w600,
+ summary.summary!,
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 15,
+ height: 1.6,
+ letterSpacing: 0.2,
),
),
],
),
- const SizedBox(height: 12),
- Text(
- summary.summary!,
- style: const TextStyle(
- color: Colors.white,
- fontSize: 15,
- height: 1.5,
- ),
- ),
- ],
- ),
- ),
- const SizedBox(height: 20),
- ],
+ ),
+ const SizedBox(height: 24),
+ ],
- // Metrics Grid
- _buildMetricsGrid(),
+ // Metrics Grid
+ _buildMetricsGrid(),
- const SizedBox(height: 20),
+ const SizedBox(height: 24),
- // Close Button
- TextButton(
- onPressed: () => Navigator.of(context).pop(),
- style: TextButton.styleFrom(
- foregroundColor: const Color(0xFF1DA1F2),
- padding: const EdgeInsets.symmetric(
- horizontal: 24,
- vertical: 12,
+ // Close Button
+ Container(
+ width: double.infinity,
+ height: 48,
+ decoration: BoxDecoration(
+ gradient: const LinearGradient(
+ colors: [Color(0xFF1DA1F2), Color(0xFF1890D5)],
+ ),
+ borderRadius: BorderRadius.circular(24),
+ boxShadow: [
+ BoxShadow(
+ color: const Color(0xFF1DA1F2).withOpacity(0.3),
+ blurRadius: 12,
+ offset: const Offset(0, 4),
+ ),
+ ],
+ ),
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: () => Navigator.of(context).pop(),
+ borderRadius: BorderRadius.circular(24),
+ child: const Center(
+ child: Text(
+ 'Close',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 16,
+ fontWeight: FontWeight.bold,
+ letterSpacing: 0.5,
+ ),
+ ),
+ ),
+ ),
+ ),
),
- ),
- child: const Text(
- 'Close',
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
- ),
+ ],
),
- ],
+ ),
),
),
);
@@ -127,31 +219,50 @@ class TweetSummaryDialog extends StatelessWidget {
'icon': Icons.visibility_outlined,
'label': 'Views',
'value': summary.views,
+ 'color': const Color(0xFF1DA1F2),
+ },
+ {
+ 'icon': Icons.favorite_border,
+ 'label': 'Likes',
+ 'value': summary.likes,
+ 'color': const Color(0xFFF91880),
},
- {'icon': Icons.favorite_border, 'label': 'Likes', 'value': summary.likes},
{
'icon': Icons.chat_bubble_outline,
'label': 'Replies',
'value': summary.replies,
+ 'color': const Color(0xFF1DA1F2),
+ },
+ {
+ 'icon': Icons.repeat,
+ 'label': 'Retweets',
+ 'value': summary.retweets,
+ 'color': const Color(0xFF00BA7C),
+ },
+ {
+ 'icon': Icons.format_quote,
+ 'label': 'Quotes',
+ 'value': summary.quotes,
+ 'color': const Color(0xFF1DA1F2),
},
- {'icon': Icons.repeat, 'label': 'Retweets', 'value': summary.retweets},
- {'icon': Icons.format_quote, 'label': 'Quotes', 'value': summary.quotes},
{
'icon': Icons.bookmark_border,
'label': 'Bookmarks',
'value': summary.bookmarks,
+ 'color': const Color(0xFF1DA1F2),
},
];
return Wrap(
- spacing: 16,
- runSpacing: 16,
+ spacing: 12,
+ runSpacing: 12,
children: metrics
.map(
(metric) => _buildMetricCard(
icon: metric['icon'] as IconData,
label: metric['label'] as String,
value: metric['value'] as int,
+ color: metric['color'] as Color,
),
)
.toList(),
@@ -162,29 +273,58 @@ class TweetSummaryDialog extends StatelessWidget {
required IconData icon,
required String label,
required int value,
+ required Color color,
}) {
return Container(
- width: 100,
- padding: const EdgeInsets.all(12),
+ width: 95,
+ padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 8),
decoration: BoxDecoration(
- color: Colors.grey[900],
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: Colors.grey[800]!, width: 1),
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [Colors.grey[900]!, Colors.grey[850]!],
+ ),
+ borderRadius: BorderRadius.circular(14),
+ border: Border.all(color: Colors.grey[800]!.withOpacity(0.5), width: 1),
+ boxShadow: [
+ BoxShadow(
+ color: color.withOpacity(0.1),
+ blurRadius: 8,
+ offset: const Offset(0, 2),
+ ),
+ ],
),
child: Column(
+ mainAxisSize: MainAxisSize.min,
children: [
- Icon(icon, color: const Color(0xFF1DA1F2), size: 28),
- const SizedBox(height: 8),
+ Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: color.withOpacity(0.1),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Icon(icon, color: color, size: 24),
+ ),
+ const SizedBox(height: 10),
Text(
_formatNumber(value),
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
+ letterSpacing: 0.5,
),
),
const SizedBox(height: 4),
- Text(label, style: TextStyle(color: Colors.grey[400], fontSize: 12)),
+ Text(
+ label,
+ style: TextStyle(
+ color: Colors.grey[400],
+ fontSize: 11,
+ fontWeight: FontWeight.w500,
+ ),
+ textAlign: TextAlign.center,
+ ),
],
),
);
diff --git a/lib/features/home/view_model/home_state.dart b/lib/features/home/view_model/home_state.dart
index 9e94e34..decf1e1 100644
--- a/lib/features/home/view_model/home_state.dart
+++ b/lib/features/home/view_model/home_state.dart
@@ -18,7 +18,7 @@ class HomeState {
this.isLoading = false,
this.error,
this.isRefreshing = false,
- this.currentFeed = FeedType.following,
+ this.currentFeed = FeedType.forYou, // Default to For You feed
});
HomeState copyWith({
diff --git a/lib/features/home/view_model/home_view_model.dart b/lib/features/home/view_model/home_view_model.dart
index ce44b9b..3f5c92a 100644
--- a/lib/features/home/view_model/home_view_model.dart
+++ b/lib/features/home/view_model/home_view_model.dart
@@ -21,7 +21,7 @@ class HomeViewModel extends Notifier {
forYouTweets: [],
followingTweets: [],
isLoading: true,
- currentFeed: FeedType.following,
+ currentFeed: FeedType.forYou, // Default to For You feed
);
}
From 771c4284fa82c176cb54acf7706e250ee1bd18ab Mon Sep 17 00:00:00 2001
From: "@kerloswagih" <14712022101143@stud.cu.edu.eg>
Date: Fri, 5 Dec 2025 12:37:22 +0200
Subject: [PATCH 08/30] added localization setup and trends ui
---
README.md | 31 ++++
l10n.yaml | 8 +
lib/core/routes/AppRouter.dart | 11 +-
lib/core/routes/Route_Constants.dart | 3 +
lib/features/profile/models/shared.dart | 2 -
lib/features/trends/models/trend_model.dart | 17 ++
.../repositories/trends_repository.dart | 5 +
.../trends/view/screens/trends_screen.dart | 70 ++++++++
.../trends/view/widgets/trend_tile.dart | 71 ++++++++
.../trends/view_model/trends_view_model.dart | 12 ++
lib/l10n/app_ar.arb | 6 +
lib/l10n/app_en.arb | 5 +
lib/l10n/app_localizations.dart | 152 ++++++++++++++++++
lib/l10n/app_localizations_ar.dart | 21 +++
lib/l10n/app_localizations_en.dart | 21 +++
lib/main.dart | 7 +
pubspec.lock | 2 +-
pubspec.yaml | 3 +
18 files changed, 443 insertions(+), 4 deletions(-)
create mode 100644 l10n.yaml
create mode 100644 lib/features/trends/models/trend_model.dart
create mode 100644 lib/features/trends/repositories/trends_repository.dart
create mode 100644 lib/features/trends/view/screens/trends_screen.dart
create mode 100644 lib/features/trends/view/widgets/trend_tile.dart
create mode 100644 lib/features/trends/view_model/trends_view_model.dart
create mode 100644 lib/l10n/app_ar.arb
create mode 100644 lib/l10n/app_en.arb
create mode 100644 lib/l10n/app_localizations.dart
create mode 100644 lib/l10n/app_localizations_ar.dart
create mode 100644 lib/l10n/app_localizations_en.dart
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/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/routes/AppRouter.dart b/lib/core/routes/AppRouter.dart
index d43296a..fe19035 100644
--- a/lib/core/routes/AppRouter.dart
+++ b/lib/core/routes/AppRouter.dart
@@ -24,6 +24,7 @@ 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';
@@ -55,7 +56,7 @@ import 'package:lite_x/features/settings/screens/ChangePassword_Screen.dart';
class Approuter {
static final GoRouter router = GoRouter(
// initialLocation: "/appshell",
- initialLocation: "/splash",
+ initialLocation: "/trends",
// initialExtra: ProfileModel(
// id: "",
// username: "hazememam",
@@ -397,6 +398,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",
diff --git a/lib/core/routes/Route_Constants.dart b/lib/core/routes/Route_Constants.dart
index 7dc85b6..c513def 100644
--- a/lib/core/routes/Route_Constants.dart
+++ b/lib/core/routes/Route_Constants.dart
@@ -51,5 +51,8 @@ class RouteConstants {
static String ExploreScreen = "ExploreScreen";
static String ExploreProfileScreen = "ExploreProfileScreen";
+ // trends feature
+ static String TrendsScreen = "TrendsScreen";
+
static String TweetDetailsScreen = "TweetDetailsScreen";
}
diff --git a/lib/features/profile/models/shared.dart b/lib/features/profile/models/shared.dart
index bd7f0df..5bbb8de 100644
--- a/lib/features/profile/models/shared.dart
+++ b/lib/features/profile/models/shared.dart
@@ -1,5 +1,3 @@
-import 'dart:ffi';
-
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
diff --git a/lib/features/trends/models/trend_model.dart b/lib/features/trends/models/trend_model.dart
new file mode 100644
index 0000000..0d18e14
--- /dev/null
+++ b/lib/features/trends/models/trend_model.dart
@@ -0,0 +1,17 @@
+// Model representing a single trend item in the Trends screen
+class TrendModel {
+ final int rank; // 1, 2, 3, ...
+ final String
+ contextLabel; // e.g. "Trending in Egypt" or "Only on X • Trending"
+ final String title; // e.g. hashtag or trend title
+ final String? postsCountLabel; // e.g. "17.4K posts" (optional)
+ final bool hasMenu; // show kebab menu icon
+
+ const TrendModel({
+ required this.rank,
+ required this.contextLabel,
+ required this.title,
+ this.postsCountLabel,
+ this.hasMenu = true,
+ });
+}
diff --git a/lib/features/trends/repositories/trends_repository.dart b/lib/features/trends/repositories/trends_repository.dart
new file mode 100644
index 0000000..faaf892
--- /dev/null
+++ b/lib/features/trends/repositories/trends_repository.dart
@@ -0,0 +1,5 @@
+// Placeholder repository for Trends feature
+// Implement local/remote methods similar to profile repositories
+class TrendsRepository {
+ TrendsRepository();
+}
diff --git a/lib/features/trends/view/screens/trends_screen.dart b/lib/features/trends/view/screens/trends_screen.dart
new file mode 100644
index 0000000..c17b550
--- /dev/null
+++ b/lib/features/trends/view/screens/trends_screen.dart
@@ -0,0 +1,70 @@
+import 'package:flutter/material.dart';
+import '../../models/trend_model.dart';
+import '../widgets/trend_tile.dart';
+
+class TrendsScreen extends StatelessWidget {
+ const TrendsScreen({super.key});
+
+ List _sampleTrends() {
+ return const [
+ TrendModel(
+ rank: 1,
+ contextLabel: 'Trending in Egypt',
+ title: '#براءة_البطل_احمد_عبدالقادر',
+ ),
+ TrendModel(
+ rank: 2,
+ contextLabel: 'Only on X • Trending',
+ title: 'ياسر ابو شاب',
+ postsCountLabel: '17.4K posts',
+ ),
+ TrendModel(
+ rank: 3,
+ contextLabel: 'Trending in Egypt',
+ title: '#الناس_تحكي_علاحداث',
+ ),
+ TrendModel(
+ rank: 4,
+ contextLabel: 'Trending in Egypt',
+ title: '#عمودياب_توب_انغامي',
+ ),
+ TrendModel(
+ rank: 5,
+ contextLabel: 'Trending in Egypt',
+ title: '#المسلماني_يصلح_المنظومه',
+ ),
+ TrendModel(
+ rank: 6,
+ contextLabel: 'Trending in Egypt',
+ title: 'خالد بن الوليد',
+ postsCountLabel: '2,508 posts',
+ ),
+ TrendModel(
+ rank: 7,
+ contextLabel: 'Trending in Egypt',
+ title: 'اليوم الخميس',
+ ),
+ ];
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final trends = _sampleTrends();
+ return Scaffold(
+ appBar: AppBar(title: const Text('Trends')),
+ body: ListView.separated(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ itemCount: trends.length,
+ itemBuilder: (context, index) => TrendTile(trend: trends[index]),
+ separatorBuilder: (context, index) => Divider(
+ height: 1,
+ color: Theme.of(context).colorScheme.onSurface.withOpacity(0.1),
+ ),
+ ),
+ floatingActionButton: FloatingActionButton(
+ onPressed: () {},
+ child: const Icon(Icons.add),
+ ),
+ );
+ }
+}
diff --git a/lib/features/trends/view/widgets/trend_tile.dart b/lib/features/trends/view/widgets/trend_tile.dart
new file mode 100644
index 0000000..d67137d
--- /dev/null
+++ b/lib/features/trends/view/widgets/trend_tile.dart
@@ -0,0 +1,71 @@
+import 'package:flutter/material.dart';
+import '../../models/trend_model.dart';
+
+class TrendTile extends StatelessWidget {
+ final TrendModel trend;
+ const TrendTile({super.key, required this.trend});
+
+ @override
+ Widget build(BuildContext context) {
+ final textTheme = Theme.of(context).textTheme;
+ final colorScheme = Theme.of(context).colorScheme;
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Rank number
+ Padding(
+ padding: const EdgeInsets.only(right: 12.0, top: 2.0),
+ child: Text(
+ '${trend.rank}',
+ style: textTheme.bodyMedium?.copyWith(
+ color: colorScheme.onSurface.withOpacity(0.7),
+ ),
+ ),
+ ),
+ // Main content
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Context label (e.g., Trending in Egypt)
+ Text(
+ trend.contextLabel,
+ style: textTheme.labelSmall?.copyWith(
+ color: colorScheme.onSurface.withOpacity(0.7),
+ ),
+ ),
+ const SizedBox(height: 4),
+ // Title
+ Text(
+ trend.title,
+ style: textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 4),
+ // Posts count
+ if (trend.postsCountLabel != null)
+ Text(
+ trend.postsCountLabel!,
+ style: textTheme.labelSmall?.copyWith(
+ color: colorScheme.onSurface.withOpacity(0.7),
+ ),
+ ),
+ ],
+ ),
+ ),
+ // Kebab menu icon
+ if (trend.hasMenu)
+ IconButton(
+ icon: const Icon(Icons.more_vert),
+ color: colorScheme.onSurface.withOpacity(0.7),
+ onPressed: () {},
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/features/trends/view_model/trends_view_model.dart b/lib/features/trends/view_model/trends_view_model.dart
new file mode 100644
index 0000000..917c2a2
--- /dev/null
+++ b/lib/features/trends/view_model/trends_view_model.dart
@@ -0,0 +1,12 @@
+import 'package:flutter/foundation.dart';
+
+class TrendsViewModel extends ChangeNotifier {
+ // Placeholder state and methods mirroring profile view model
+ List trends = [];
+
+ void loadTrends() {
+ // TODO: implement loading logic
+ trends = [];
+ notifyListeners();
+ }
+}
diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb
new file mode 100644
index 0000000..bdb9be6
--- /dev/null
+++ b/lib/l10n/app_ar.arb
@@ -0,0 +1,6 @@
+{
+ "@locale": "ar",
+ "appTitle": "اكس لايت",
+ "trendsTitle": "المتصدر",
+ "postsCount": "{count} منشور"
+}
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
new file mode 100644
index 0000000..231ea4d
--- /dev/null
+++ b/lib/l10n/app_en.arb
@@ -0,0 +1,5 @@
+{
+ "appTitle": "X Lite",
+ "trendsTitle": "Trends",
+ "postsCount": "{count} posts"
+}
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
new file mode 100644
index 0000000..06793ef
--- /dev/null
+++ b/lib/l10n/app_localizations.dart
@@ -0,0 +1,152 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_localizations/flutter_localizations.dart';
+import 'package:intl/intl.dart' as intl;
+
+import 'app_localizations_ar.dart';
+import 'app_localizations_en.dart';
+
+// ignore_for_file: type=lint
+
+/// Callers can lookup localized strings with an instance of AppLocalizations
+/// returned by `AppLocalizations.of(context)`.
+///
+/// Applications need to include `AppLocalizations.delegate()` in their app's
+/// `localizationDelegates` list, and the locales they support in the app's
+/// `supportedLocales` list. For example:
+///
+/// ```dart
+/// import 'l10n/app_localizations.dart';
+///
+/// return MaterialApp(
+/// localizationsDelegates: AppLocalizations.localizationsDelegates,
+/// supportedLocales: AppLocalizations.supportedLocales,
+/// home: MyApplicationHome(),
+/// );
+/// ```
+///
+/// ## Update pubspec.yaml
+///
+/// Please make sure to update your pubspec.yaml to include the following
+/// packages:
+///
+/// ```yaml
+/// dependencies:
+/// # Internationalization support.
+/// flutter_localizations:
+/// sdk: flutter
+/// intl: any # Use the pinned version from flutter_localizations
+///
+/// # Rest of dependencies
+/// ```
+///
+/// ## iOS Applications
+///
+/// iOS applications define key application metadata, including supported
+/// locales, in an Info.plist file that is built into the application bundle.
+/// To configure the locales supported by your app, you’ll need to edit this
+/// file.
+///
+/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
+/// Then, in the Project Navigator, open the Info.plist file under the Runner
+/// project’s Runner folder.
+///
+/// Next, select the Information Property List item, select Add Item from the
+/// Editor menu, then select Localizations from the pop-up menu.
+///
+/// Select and expand the newly-created Localizations item then, for each
+/// locale your application supports, add a new item and select the locale
+/// you wish to add from the pop-up menu in the Value field. This list should
+/// be consistent with the languages listed in the AppLocalizations.supportedLocales
+/// property.
+abstract class AppLocalizations {
+ AppLocalizations(String locale)
+ : localeName = intl.Intl.canonicalizedLocale(locale.toString());
+
+ final String localeName;
+
+ static AppLocalizations of(BuildContext context) {
+ return Localizations.of(context, AppLocalizations)!;
+ }
+
+ static const LocalizationsDelegate delegate =
+ _AppLocalizationsDelegate();
+
+ /// A list of this localizations delegate along with the default localizations
+ /// delegates.
+ ///
+ /// Returns a list of localizations delegates containing this delegate along with
+ /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
+ /// and GlobalWidgetsLocalizations.delegate.
+ ///
+ /// Additional delegates can be added by appending to this list in
+ /// MaterialApp. This list does not have to be used at all if a custom list
+ /// of delegates is preferred or required.
+ static const List> localizationsDelegates =
+ >[
+ delegate,
+ GlobalMaterialLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ ];
+
+ /// A list of this localizations delegate's supported locales.
+ static const List supportedLocales = [
+ Locale('ar'),
+ Locale('en'),
+ ];
+
+ /// No description provided for @appTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'X Lite'**
+ String get appTitle;
+
+ /// No description provided for @trendsTitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Trends'**
+ String get trendsTitle;
+
+ /// No description provided for @postsCount.
+ ///
+ /// In en, this message translates to:
+ /// **'{count} posts'**
+ String postsCount(Object count);
+}
+
+class _AppLocalizationsDelegate
+ extends LocalizationsDelegate {
+ const _AppLocalizationsDelegate();
+
+ @override
+ Future load(Locale locale) {
+ return SynchronousFuture(lookupAppLocalizations(locale));
+ }
+
+ @override
+ bool isSupported(Locale locale) =>
+ ['ar', 'en'].contains(locale.languageCode);
+
+ @override
+ bool shouldReload(_AppLocalizationsDelegate old) => false;
+}
+
+AppLocalizations lookupAppLocalizations(Locale locale) {
+ // Lookup logic when only language code is specified.
+ switch (locale.languageCode) {
+ case 'ar':
+ return AppLocalizationsAr();
+ case 'en':
+ return AppLocalizationsEn();
+ }
+
+ throw FlutterError(
+ 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
+ 'an issue with the localizations generation tool. Please file an issue '
+ 'on GitHub with a reproducible sample app and the gen-l10n configuration '
+ 'that was used.',
+ );
+}
diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart
new file mode 100644
index 0000000..16f80a2
--- /dev/null
+++ b/lib/l10n/app_localizations_ar.dart
@@ -0,0 +1,21 @@
+// ignore: unused_import
+import 'package:intl/intl.dart' as intl;
+import 'app_localizations.dart';
+
+// ignore_for_file: type=lint
+
+/// The translations for Arabic (`ar`).
+class AppLocalizationsAr extends AppLocalizations {
+ AppLocalizationsAr([String locale = 'ar']) : super(locale);
+
+ @override
+ String get appTitle => 'اكس لايت';
+
+ @override
+ String get trendsTitle => 'المتصدر';
+
+ @override
+ String postsCount(Object count) {
+ return '$count منشور';
+ }
+}
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
new file mode 100644
index 0000000..db43e12
--- /dev/null
+++ b/lib/l10n/app_localizations_en.dart
@@ -0,0 +1,21 @@
+// ignore: unused_import
+import 'package:intl/intl.dart' as intl;
+import 'app_localizations.dart';
+
+// ignore_for_file: type=lint
+
+/// The translations for English (`en`).
+class AppLocalizationsEn extends AppLocalizations {
+ AppLocalizationsEn([String locale = 'en']) : super(locale);
+
+ @override
+ String get appTitle => 'X Lite';
+
+ @override
+ String get trendsTitle => 'Trends';
+
+ @override
+ String postsCount(Object count) {
+ return '$count posts';
+ }
+}
diff --git a/lib/main.dart b/lib/main.dart
index 16a75be..3061310 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,5 +1,6 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_ce_flutter/hive_flutter.dart';
@@ -50,6 +51,12 @@ class MyApp extends StatelessWidget {
debugShowCheckedModeBanner: false,
title: 'X Lite',
theme: appTheme,
+ localizationsDelegates: const [
+ GlobalMaterialLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ ],
+ supportedLocales: const [Locale('en'), Locale('ar')],
routerConfig: Approuter.router,
);
}
diff --git a/pubspec.lock b/pubspec.lock
index 7c1bc15..897efde 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -735,7 +735,7 @@ packages:
source: hosted
version: "0.14.4"
flutter_localizations:
- dependency: transitive
+ dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 78f3670..def37f7 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -10,6 +10,8 @@ environment:
dependencies:
flutter:
sdk: flutter
+ flutter_localizations:
+ sdk: flutter
cupertino_icons: ^1.0.8
flutter_riverpod: ^3.0.0
@@ -88,6 +90,7 @@ dev_dependencies:
# The following section is specific to Flutter packages.
riverpod: any
flutter:
+ generate: true
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
From 1180c774e1bb19a4e70e3ef66f0de9ad37941fad Mon Sep 17 00:00:00 2001
From: yaraFarouk
Date: Fri, 5 Dec 2025 13:06:40 +0200
Subject: [PATCH 09/30] video done
---
android/app/build.gradle.kts | 3 +-
lib/core/classes/PickedImage.dart | 18 +
.../home/view/screens/create_post_screen.dart | 177 ++++++++--
.../view/screens/quote_composer_screen.dart | 156 +++++++--
.../view/screens/reply_composer_screen.dart | 326 ++++++++++++++++--
.../view/widgets/inline_video_player.dart | 211 ++++++++++++
.../home/view/widgets/media_gallery.dart | 47 ++-
.../view/widgets/video_player_screen.dart | 218 ++++++++++++
macos/Flutter/GeneratedPluginRegistrant.swift | 2 +
pubspec.lock | 48 +++
pubspec.yaml | 3 +
11 files changed, 1130 insertions(+), 79 deletions(-)
create mode 100644 lib/features/home/view/widgets/inline_video_player.dart
create mode 100644 lib/features/home/view/widgets/video_player_screen.dart
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index e792664..15f5222 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -25,7 +25,8 @@ android {
applicationId = "com.Artemsia.lite_x"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
- minSdk = flutter.minSdkVersion
+ // Set minSdk to 24 for better ExoPlayer/Media3 video codec support
+ minSdk = 24
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
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/features/home/view/screens/create_post_screen.dart b/lib/features/home/view/screens/create_post_screen.dart
index 2338a41..91266cd 100644
--- a/lib/features/home/view/screens/create_post_screen.dart
+++ b/lib/features/home/view/screens/create_post_screen.dart
@@ -61,7 +61,8 @@ class _CreatePostScreenState extends ConsumerState {
final TextEditingController _textController = TextEditingController();
final FocusNode _focusNode = FocusNode();
bool _isPosting = false;
- final List _selectedImages = [];
+ final List _selectedMedia =
+ []; // Changed from _selectedImages to support both images and videos
PostPrivacy _selectedPrivacy = PostPrivacy.everyone;
@override
@@ -91,14 +92,14 @@ class _CreatePostScreenState extends ConsumerState {
try {
List mediaIds = [];
- if (_selectedImages.isNotEmpty) {
- final uploadedIds = await upload_media(_selectedImages);
+ if (_selectedMedia.isNotEmpty) {
+ final uploadedIds = await upload_media(_selectedMedia);
mediaIds = uploadedIds.where((id) => id.isNotEmpty).toList();
- if (mediaIds.length != _selectedImages.length && mounted) {
+ if (mediaIds.length != _selectedMedia.length && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
- content: Text('Some images failed to upload. Try again.'),
+ content: Text('Some media files failed to upload. Try again.'),
behavior: SnackBarBehavior.floating,
backgroundColor: Colors.orange,
),
@@ -106,7 +107,7 @@ class _CreatePostScreenState extends ConsumerState {
}
if (mediaIds.isEmpty) {
- throw Exception('Unable to upload selected images.');
+ throw Exception('Unable to upload selected media.');
}
}
@@ -122,7 +123,7 @@ class _CreatePostScreenState extends ConsumerState {
if (mounted) {
setState(() {
_textController.clear();
- _selectedImages.clear();
+ _selectedMedia.clear();
});
Navigator.pop(context, true);
ScaffoldMessenger.of(context).showSnackBar(
@@ -157,11 +158,11 @@ class _CreatePostScreenState extends ConsumerState {
}
Future _pickImage() async {
- final remainingSlots = 4 - _selectedImages.length;
+ final remainingSlots = 4 - _selectedMedia.length;
if (remainingSlots <= 0) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
- content: Text('Maximum 4 images allowed per post.'),
+ content: Text('Maximum 4 media files allowed per post.'),
behavior: SnackBarBehavior.floating,
),
);
@@ -174,29 +175,110 @@ class _CreatePostScreenState extends ConsumerState {
setState(() {
for (final picked in pickedList) {
if (picked.file != null) {
- _selectedImages.add(picked.file!);
+ _selectedMedia.add(picked.file!);
}
}
});
}
- void _removeImage(int index) {
- if (index < 0 || index >= _selectedImages.length) return;
+ Future _pickVideo() async {
+ final remainingSlots = 4 - _selectedMedia.length;
+ if (remainingSlots <= 0) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Maximum 4 media files allowed per post.'),
+ behavior: SnackBarBehavior.floating,
+ ),
+ );
+ return;
+ }
+
+ final picked = await pickVideo();
+ if (picked == null || picked.file == null) return;
+
+ setState(() {
+ _selectedMedia.add(picked.file!);
+ });
+ }
+
+ void _showMediaPicker() {
+ showModalBottomSheet(
+ context: context,
+ backgroundColor: const Color(0xFF1E1E1E),
+ shape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ builder: (context) => SafeArea(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const SizedBox(height: 12),
+ Container(
+ width: 40,
+ height: 4,
+ decoration: BoxDecoration(
+ color: Colors.grey[600],
+ borderRadius: BorderRadius.circular(2),
+ ),
+ ),
+ const SizedBox(height: 20),
+ ListTile(
+ leading: const Icon(Icons.image, color: Color(0xFF1D9BF0)),
+ title: const Text('Photo', style: TextStyle(color: Colors.white)),
+ onTap: () {
+ Navigator.pop(context);
+ _pickImage();
+ },
+ ),
+ ListTile(
+ leading: const Icon(Icons.videocam, color: Color(0xFF1D9BF0)),
+ title: const Text('Video', style: TextStyle(color: Colors.white)),
+ onTap: () {
+ Navigator.pop(context);
+ _pickVideo();
+ },
+ ),
+ const SizedBox(height: 16),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _removeMedia(int index) {
+ if (index < 0 || index >= _selectedMedia.length) return;
setState(() {
- _selectedImages.removeAt(index);
+ _selectedMedia.removeAt(index);
});
}
- Widget _buildSelectedImagesPreview() {
- if (_selectedImages.isEmpty) return const SizedBox.shrink();
- final crossAxisCount = _selectedImages.length == 1 ? 1 : 2;
- final double aspectRatio = _selectedImages.length == 1 ? 16 / 9 : 1;
+ bool _isVideoFile(File file) {
+ final extension = file.path.split('.').last.toLowerCase();
+ return [
+ 'mp4',
+ 'mov',
+ 'avi',
+ 'webm',
+ 'mkv',
+ 'flv',
+ 'wmv',
+ 'mpeg',
+ 'mpg',
+ '3gp',
+ 'm4v',
+ ].contains(extension);
+ }
+
+ Widget _buildSelectedMediaPreview() {
+ if (_selectedMedia.isEmpty) return const SizedBox.shrink();
+ final crossAxisCount = _selectedMedia.length == 1 ? 1 : 2;
+ final double aspectRatio = _selectedMedia.length == 1 ? 16 / 9 : 1;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
- '${_selectedImages.length} / 4 photos',
+ '${_selectedMedia.length} / 4 media files',
style: TextStyle(color: Colors.grey[500], fontSize: 13),
),
const SizedBox(height: 8),
@@ -209,22 +291,65 @@ class _CreatePostScreenState extends ConsumerState {
mainAxisSpacing: 8,
childAspectRatio: aspectRatio,
),
- itemCount: _selectedImages.length,
+ itemCount: _selectedMedia.length,
itemBuilder: (context, index) {
- final file = _selectedImages[index];
+ final file = _selectedMedia[index];
+ final isVideo = _isVideoFile(file);
+
return Stack(
children: [
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
- child: Image.file(file, fit: BoxFit.cover),
+ child: isVideo
+ ? Container(
+ color: Colors.grey[900],
+ child: const Center(
+ child: Icon(
+ Icons.play_circle_outline,
+ color: Colors.white,
+ size: 64,
+ ),
+ ),
+ )
+ : Image.file(file, fit: BoxFit.cover),
),
),
+ if (isVideo)
+ Positioned(
+ bottom: 8,
+ left: 8,
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 6,
+ vertical: 2,
+ ),
+ decoration: BoxDecoration(
+ color: Colors.black87,
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: const Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(Icons.videocam, color: Colors.white, size: 12),
+ SizedBox(width: 4),
+ Text(
+ 'Video',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 11,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
Positioned(
top: 8,
right: 8,
child: InkWell(
- onTap: () => _removeImage(index),
+ onTap: () => _removeMedia(index),
child: Container(
decoration: const BoxDecoration(
color: Colors.black54,
@@ -485,11 +610,11 @@ class _CreatePostScreenState extends ConsumerState {
),
],
),
- if (_selectedImages.isNotEmpty) ...[
+ if (_selectedMedia.isNotEmpty) ...[
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.only(left: 52),
- child: _buildSelectedImagesPreview(),
+ child: _buildSelectedMediaPreview(),
),
],
const SizedBox(height: 12),
@@ -539,10 +664,10 @@ class _CreatePostScreenState extends ConsumerState