chat screen

This commit is contained in:
Felipe
2024-04-16 08:46:06 -05:00
parent 34c60af688
commit 61d517aaea
18 changed files with 931 additions and 22 deletions
+63
View File
@@ -0,0 +1,63 @@
import 'dart:developer';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:chat_repository/chat_repository.dart';
part 'chat_event.dart';
part 'chat_state.dart';
class ChatBloc extends Bloc<ChatEvent, ChatState> {
final FirebaseChatRepository _chatRepository;
ChatBloc({
required FirebaseChatRepository chatRepository,
}) : _chatRepository = chatRepository,
super(ChatInitial()) {
on<LoadChatEvent>(_onLoadChatEvent);
on<SendMessageEvent>(_onSendMessageEvent);
}
void _onLoadChatEvent(LoadChatEvent event, Emitter<ChatState> emit) async {
bool autoCreate = true;
try {
emit(ChatLoading());
Stream<ChatEntity?> chatStream = _chatRepository.getChatById(
event.serviceId,
);
await for (var chat in chatStream) {
if (chat == null) {
if (autoCreate) {
chat = await _chatRepository.createNewChat(
event.serviceId,
event.userId,
event.professionalId,
);
autoCreate = false;
} else {
emit(ChatFailure());
}
} else {
emit(ChatLoaded(chat: chat));
}
}
} catch (e) {
log(e.toString());
emit(ChatFailure());
}
}
void _onSendMessageEvent(
SendMessageEvent event, Emitter<ChatState> emit) async {
try {
await _chatRepository.sendMessage(event.serviceId, event.message);
} catch (e) {
log(e.toString());
emit(ChatFailure());
}
}
}
+43
View File
@@ -0,0 +1,43 @@
part of 'chat_bloc.dart';
abstract class ChatEvent extends Equatable {
const ChatEvent();
@override
List<Object> get props => [];
}
class LoadChatEvent extends ChatEvent {
final String serviceId;
final String userId;
final String professionalId;
const LoadChatEvent({
required this.serviceId,
required this.userId,
required this.professionalId,
});
@override
List<Object> get props => [
serviceId,
userId,
professionalId,
];
}
class SendMessageEvent extends ChatEvent {
final String serviceId;
final MessageEntity message;
const SendMessageEvent({
required this.message,
required this.serviceId,
});
@override
List<Object> get props => [
serviceId,
message,
];
}
+23
View File
@@ -0,0 +1,23 @@
part of 'chat_bloc.dart';
class ChatState extends Equatable {
const ChatState();
@override
List<Object> get props => [];
}
class ChatInitial extends ChatState {}
class ChatLoading extends ChatState {}
class ChatLoaded extends ChatState {
final ChatEntity chat;
const ChatLoaded({required this.chat});
@override
List<Object> get props => [chat];
}
class ChatFailure extends ChatState {}
+7
View File
@@ -1,9 +1,11 @@
import 'package:chat_repository/chat_repository.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:injector/injector.dart';
import 'package:profession_repository/profession_repository.dart';
import 'package:professional_repository/professional_repository.dart';
import 'package:prosappco/blocs/auth_bloc/auth_bloc.dart';
import 'package:prosappco/blocs/authentication_bloc/authentication_bloc.dart';
import 'package:prosappco/blocs/chat_bloc/chat_bloc.dart';
import 'package:prosappco/blocs/my_user_bloc/my_user_bloc.dart';
import 'package:prosappco/blocs/professional_bloc/professional_bloc.dart';
import 'package:prosappco/blocs/professional_list_bloc/professional_list_bloc.dart';
@@ -35,6 +37,7 @@ class AppDI {
injector.registerSingleton(() => FirebaseProfessionalRepository());
injector.registerSingleton(() => FirebaseServiceRepository());
injector.registerSingleton(() => FirebaseChatRepository());
injector.registerSingleton<AuthenticationBloc>((() =>
AuthenticationBloc(myUserRepository: injector.get<UserRepository>())));
@@ -73,6 +76,10 @@ class AppDI {
injector.get<FirebaseProfessionalRepository>()),
);
injector.registerDependency<ChatBloc>(() => ChatBloc(
chatRepository: injector.get<FirebaseChatRepository>(),
));
injector.registerDependency<ServiceBloc>(
() => ServiceBloc(
serviceRepository: injector.get<FirebaseServiceRepository>(),
+329
View File
@@ -0,0 +1,329 @@
import 'package:chat_repository/chat_repository.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injector/injector.dart';
import 'package:intl/intl.dart';
import 'package:prosappco/blocs/chat_bloc/chat_bloc.dart';
import 'package:prosappco/screens/user/user_view_profile_screen.dart';
import 'package:service_repository/service_repository.dart';
import 'package:user_repository/user_repository.dart';
class ChatScreen extends StatefulWidget {
final ServiceEntity service;
const ChatScreen({super.key, required this.service});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
late final ChatBloc chatBloc;
final TextEditingController _messageController = TextEditingController();
final FocusNode _messageFocusNode = FocusNode();
@override
void initState() {
super.initState();
chatBloc = Injector.appInstance.get<ChatBloc>();
chatBloc.add(LoadChatEvent(
serviceId: widget.service.id!,
userId: widget.service.userId,
professionalId: widget.service.professionalId,
));
}
@override
void dispose() {
_messageController.dispose();
_messageFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Chat'),
),
body: BlocProvider(
create: (context) => chatBloc,
child: BlocBuilder<ChatBloc, ChatState>(
builder: (context, state) {
if (state is ChatLoaded) {
return Column(
children: [
FutureBuilder(
future: _getUserAndProfessionalInfo(widget.service),
builder: (BuildContext context,
AsyncSnapshot<List<dynamic>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else {
if (snapshot.hasError) {
return Center(
child: Text('Error inesperado: ${snapshot.error}'),
);
} else {
final userInfo = snapshot.data![0] as MyUser;
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
color: const Color(0xFFD6F4FF),
alignment: Alignment.topCenter,
child: ListTile(
leading: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.grey.shade300,
shape: BoxShape.circle,
image: userInfo.picture == null
? null
: DecorationImage(
image: NetworkImage(
userInfo.picture ?? ''),
fit: BoxFit.contain,
),
),
child: userInfo.picture == null
? Icon(
CupertinoIcons.person,
color: Colors.grey.shade400,
size: 40,
)
: null,
),
title: Text(
userInfo.name ?? '',
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
subtitle: const Text(
'Rating: 5.0',
// _disponibilidad(
// filteredUsers[index].professionalInfo),
style: TextStyle(
fontSize: 13,
color: Colors.blue,
),
),
),
);
}
}
},
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.only(top: 10),
reverse: true,
child: Column(
children: _messagesList(state.chat.messages),
),
),
),
Container(
alignment: Alignment.bottomCenter,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 15, vertical: 15),
child: Row(
children: [
Expanded(
child: TextFormField(
focusNode: _messageFocusNode,
controller: _messageController,
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
hintText: 'Mensaje',
hintStyle: TextStyle(
color: Colors.grey[600], fontSize: 16),
border: OutlineInputBorder(
borderSide: const BorderSide(
color: Colors.grey, width: 1.0),
borderRadius: BorderRadius.circular(50)),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: Colors.grey, width: 1.0),
borderRadius: BorderRadius.circular(50)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 15),
filled: true,
fillColor: Colors.grey[200],
),
onFieldSubmitted: (value) async {
if (_messageController.text.isEmpty) {
return;
}
MessageEntity message = MessageEntity(
ownerId:
FirebaseAuth.instance.currentUser!.uid,
content: _messageController.text.trim(),
createdAt: DateTime.now(),
);
chatBloc.add(SendMessageEvent(
serviceId: widget.service.id!,
message: message,
));
_messageController.clear();
_messageFocusNode.requestFocus();
},
),
),
const SizedBox(width: 12),
GestureDetector(
onTap: () async {
if (_messageController.text.isEmpty) {
return;
}
MessageEntity message = MessageEntity(
ownerId: FirebaseAuth.instance.currentUser!.uid,
content: _messageController.text.trim(),
createdAt: DateTime.now(),
);
chatBloc.add(SendMessageEvent(
serviceId: widget.service.id!,
message: message,
));
_messageController.clear();
_messageFocusNode.requestFocus();
},
child: Container(
height: 50,
width: 50,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
child: const Center(
child: Icon(
Icons.send,
color: Colors.white,
),
),
),
)
],
),
),
)
],
);
}
return const Center(
child: CircularProgressIndicator(),
);
},
),
),
);
}
List<Widget> _messagesList(List<MessageEntity> messages) {
return messages
.map(
(e) => e.ownerId != FirebaseAuth.instance.currentUser!.uid
? ListTile(
title: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.only(right: 60),
padding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 16),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(20),
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Text(
e.content,
style: const TextStyle(fontSize: 16),
),
),
const SizedBox(width: 5),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Text(
DateFormat('h:mm a').format(e.createdAt),
style:
const TextStyle(color: Colors.grey, fontSize: 12),
),
),
],
),
)
: ListTile(
title: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
margin: const EdgeInsets.only(left: 60),
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
),
decoration: const BoxDecoration(
color: Color(0xFFD5EFFF),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Text(
e.content,
style: const TextStyle(fontSize: 16),
),
),
const SizedBox(width: 5),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Text(
DateFormat('h:mm a').format(e.createdAt),
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
),
),
),
],
),
),
)
.toList();
}
Future<List<dynamic>> _getUserAndProfessionalInfo(
ServiceEntity service) async {
final userRepo = FirebaseUserRepository(FirebaseAuth.instance);
final MyUser? userInfo;
if (FirebaseAuth.instance.currentUser!.uid == service.userId) {
userInfo = await userRepo.getMyUser(service.userId);
} else {
userInfo = await userRepo.getMyUser(service.professionalId);
}
return [userInfo];
}
}
@@ -8,6 +8,7 @@ import 'package:intl/intl.dart';
import 'package:professional_repository/professional_repository.dart';
import 'package:prosappco/blocs/service_bloc/service_bloc.dart';
import 'package:prosappco/components/general_secondary_button.dart';
import 'package:prosappco/screens/chat/chat_screen.dart';
import 'package:service_repository/service_repository.dart';
import 'package:setting_repository/setting_repository.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -55,8 +56,7 @@ class _ProfessionalServiceScreenState extends State<ProfessionalServiceScreen> {
final service = state.service;
return FutureBuilder(
future: _getUserAndProfessionalInfo(service),
builder: (BuildContext context,
AsyncSnapshot<List<dynamic>> snapshot) {
builder: (BuildContext context, AsyncSnapshot<List<dynamic>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else {
@@ -274,7 +274,16 @@ class _ProfessionalServiceScreenState extends State<ProfessionalServiceScreen> {
),
),
ElevatedButton(
onPressed: () {},
onPressed: () {
if (service.id != null) {
Navigator.push(
context,
CupertinoPageRoute(
builder: (context) => ChatScreen(service: service),
),
);
}
},
style: ElevatedButton.styleFrom(
foregroundColor: const Color(0xFF2BA4EC),
backgroundColor: Colors.white,
@@ -661,8 +670,7 @@ class _ProfessionalServiceScreenState extends State<ProfessionalServiceScreen> {
return '\$${formatter.format(number)}';
}
Future<List<dynamic>> _getUserAndProfessionalInfo(
ServiceEntity service) async {
Future<List<dynamic>> _getUserAndProfessionalInfo(ServiceEntity service) async {
final userRepo = FirebaseUserRepository(FirebaseAuth.instance);
final userInfo = await userRepo.getMyUser(service.userId);
+9 -2
View File
@@ -7,6 +7,7 @@ import 'package:injector/injector.dart';
import 'package:intl/intl.dart';
import 'package:professional_repository/professional_repository.dart';
import 'package:prosappco/blocs/service_bloc/service_bloc.dart';
import 'package:prosappco/screens/chat/chat_screen.dart';
import 'package:prosappco/components/general_secondary_button.dart';
import 'package:service_repository/service_repository.dart';
import 'package:setting_repository/setting_repository.dart';
@@ -238,7 +239,6 @@ class _UserServiceScreenState extends State<UserServiceScreen> {
customMapButton(service),
// ListTile(
// leading: const Icon(Icons.near_me),
// title: Text(
@@ -423,7 +423,14 @@ class _UserServiceScreenState extends State<UserServiceScreen> {
),
),
ElevatedButton(
onPressed: () {},
onPressed: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (context) => ChatScreen(service: service),
),
);
},
style: ElevatedButton.styleFrom(
foregroundColor: const Color(0xFF2BA4EC),
backgroundColor: Colors.white,
@@ -27,19 +27,6 @@ class UserViewProfileScreen extends StatelessWidget {
],
),
);
// Container(
// decoration: BoxDecoration(
// gradient: LinearGradient(
// colors: [
// Colors.blue.withOpacity(1),
// Colors.blue.withOpacity(0),
// Colors.blue.withOpacity(0),
// ],
// begin: Alignment.bottomCenter,
// end: Alignment.topCenter,
// ),
// ),
}
Stack buildTop() {