chats have been updated to work with consultatons

This commit is contained in:
Username1548
2025-03-14 19:56:55 +07:00
parent 9d53760137
commit 803588a289
26 changed files with 957 additions and 281 deletions

View File

@@ -86,11 +86,12 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}
}
Future<void> updateChatRoom(String chatId) async{
Future<void> cancelChosenPlan({required String chatId, required int planId}) async {
try {
final chatRoom = await chatRepository.getChatRoom(chatId);
await chatRepository.cancelChosenPlan(id: planId);
final chatRoom = await chatRepository.getChatRoom(chatId);
emit(
(state as ChatRoomStateLoaded).copyWith(chatRoom: chatRoom),
(state as ChatRoomStateLoaded).copyWith(chatRoom: chatRoom),
);
} catch (e) {
emit(ChatRoomState.error(errorMessage: 'Ошибка получеия данных'));
@@ -104,7 +105,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
if (client == null) return;
await wsManager.initPrivateDirectChatChannel(chatId);
wsManager.subscribeDirectChatChannel();
wsManager.directChatStream?.listen((event) {
messagesSubscription = wsManager.directChatStream?.listen((event) {
AudioPlayer().play((AssetSource('audio/recive_message.wav')));
final message = ChatMessageModel.fromJson(jsonDecode(event.data)['message']);
List<ChatMessageModel> messagesList = [
@@ -122,7 +123,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
);
});
wsManager.clientTypingStream?.listen((event) {
typingSubscription = wsManager.clientTypingStream?.listen((event) {
emit(
ChatRoomState.loaded(
chatRoom: (state as ChatRoomStateLoaded).chatRoom,
@@ -191,6 +192,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
@override
Future<void> close() async {
wsManager.unsubscribeDirectChatChannel();
messagesSubscription?.cancel();
typingSubscription?.cancel();
super.close();
}
}

View File

@@ -11,9 +11,13 @@ import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/order_card.dart';
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:intl/intl.dart';
import 'package:visibility_detector/visibility_detector.dart';
@@ -34,6 +38,7 @@ class _ChatScreenState extends State<ChatScreen> {
final ChatRoomCubit chatRoomCubit = sl<ChatRoomCubit>();
final List<({int id, int index})> unreadMessagesIds = [];
final List<({int id, int index})> unreadMessagesIdsCopy = [];
final showableChatBadgeStates = ChosenPlanStateEnum.getShowableChatBadgeStates();
@override
void initState() {
@@ -51,6 +56,7 @@ class _ChatScreenState extends State<ChatScreen> {
@override
void dispose() {
scrollController.dispose();
chatRoomCubit.close();
super.dispose();
}
@@ -78,6 +84,9 @@ class _ChatScreenState extends State<ChatScreen> {
}
_buildChat(BuildContext context, ChatRoomStateLoaded state) {
final chosenPlan = state.chatRoom.chosenPlan;
final amIConsultant = chosenPlan?.consultantId == state.chatRoom.users.sender.id;
final showAboutOrderButton = showableChatBadgeStates.contains(chosenPlan?.chosenPlanState);
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
@@ -86,12 +95,12 @@ class _ChatScreenState extends State<ChatScreen> {
centerTitle: false,
title: Row(
children: [
if (state.chatRoom.users['to_chat_user']!.profilePhotoUrl != null)
if (state.chatRoom.users.receiver.profilePhotoUrl != null)
CircleAvatar(
radius: 20,
foregroundImage: NetworkImage(state.chatRoom.users['to_chat_user']!.profilePhotoUrl!),
foregroundImage: NetworkImage(state.chatRoom.users.receiver.profilePhotoUrl!),
),
if (state.chatRoom.users['to_chat_user']!.profilePhotoUrl == null)
if (state.chatRoom.users.receiver.profilePhotoUrl == null)
Container(
width: 40,
height: 40,
@@ -101,7 +110,7 @@ class _ChatScreenState extends State<ChatScreen> {
shape: BoxShape.circle,
),
child: Text(
state.chatRoom.users['to_chat_user']!.name.split(' ').first[0] + state.chatRoom.users['to_chat_user']!.name.split(' ')[1][0],
state.chatRoom.users.receiver.name.split(' ').first[0] + state.chatRoom.users.receiver.name.split(' ')[1][0],
style: TextStyle(
color: Color(0xFF7F9CF5),
fontWeight: FontWeight.w400,
@@ -114,7 +123,7 @@ class _ChatScreenState extends State<ChatScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
state.chatRoom.users['to_chat_user']!.name,
state.chatRoom.users.receiver.name,
style: CustomTextStyles.titleSmallBold.copyWith(
color: PrimaryColors().black,
),
@@ -136,28 +145,38 @@ class _ChatScreenState extends State<ChatScreen> {
),
),
body: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
child: Stack(
children: [
Expanded(
child: Container(
color: appTheme.whiteA700,
width: double.maxFinite,
child: CustomScrollView(
physics: BouncingScrollPhysics(),
controller: scrollController,
reverse: true,
slivers: [
SliverPadding(
padding: EdgeInsets.symmetric(vertical: 16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index < state.chatMessages.length) {
Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Container(
color: appTheme.whiteA700,
width: double.maxFinite,
child: SingleChildScrollView(
physics: BouncingScrollPhysics(),
controller: scrollController,
reverse: true,
child: Column(
children: [
SizedBox(height: 16),
ListView.builder(
physics: NeverScrollableScrollPhysics(),
reverse: true,
shrinkWrap: true,
itemBuilder: (context, index) {
if (index == state.chatMessages.length) {
return Padding(
padding: const EdgeInsets.all(8),
child: const Loader(),
);
}
if ((index + 1 != state.chatMessages.length &&
state.chatMessages[index].updatedAt.day != state.chatMessages[index + 1].updatedAt.day) ||
index + 1 == state.chatMessages.length) {
final isCurrentUserAuthor = state.chatRoom.users['to_chat_user']!.id != state.chatMessages[index].authorId;
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
return Column(
children: [
_messageDate(state.chatMessages[index].updatedAt),
@@ -191,7 +210,7 @@ class _ChatScreenState extends State<ChatScreen> {
],
);
}
final isCurrentUserAuthor = state.chatRoom.users['to_chat_user']!.id != state.chatMessages[index].authorId;
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
return VisibilityDetector(
key: Key(state.chatMessages[index].id.toString()),
onVisibilityChanged: (VisibilityInfo info) {
@@ -217,157 +236,238 @@ class _ChatScreenState extends State<ChatScreen> {
mediaData: state.chatMessages[index].mediaData ?? [],
),
);
} else {
return Padding(
padding: const EdgeInsets.all(8),
child: const Loader(),
);
}
},
childCount: state.chatMessages.length + (state.isLoading ? 1 : 0),
),
},
itemCount: state.chatMessages.length + (state.isLoading ? 1 : 0),
),
SizedBox(height: 16),
if (chosenPlan != null)
OrderCard(
plan: chosenPlan.plan,
isUserView: !amIConsultant,
optionType: chosenPlan.optionType,
onCancel: () async {
await context.read<ChatRoomCubit>().cancelChosenPlan(chatId: state.chatRoom.id, planId: chosenPlan.id);
},
),
// switch (chosenPlan.chosenPlanState) {
// // TODO: Handle this case.
// ChosenPlanStateEnum.stateInited => throw UnimplementedError(),
// // TODO: Handle this case.
// ChosenPlanStateEnum.stateStartDateHasBeenSet => throw UnimplementedError(),
// // TODO: Handle this case.
// ChosenPlanStateEnum.stateInCoordination => throw UnimplementedError(),
// // TODO: Handle this case.
// ChosenPlanStateEnum.stateStarted => throw UnimplementedError(),
// // TODO: Handle this case.
// ChosenPlanStateEnum.stateInProgress => throw UnimplementedError(),
// // TODO: Handle this case.
// ChosenPlanStateEnum.statePendingPayment => throw UnimplementedError(),
// // TODO: Handle this case.
// ChosenPlanStateEnum.stateConfirmedByConsultant => throw UnimplementedError(),
// // TODO: Handle this case.
// ChosenPlanStateEnum.statePaid => throw UnimplementedError(),
// // TODO: Handle this case.
// ChosenPlanStateEnum.stateCanceledByTheInitiator => throw UnimplementedError(),
// // TODO: Handle this case.
// ChosenPlanStateEnum.stateLastDayStarted => throw UnimplementedError(),
// // TODO: Handle this case.
// ChosenPlanStateEnum.stateLastDayFinished => throw UnimplementedError(),
// // TODO: Handle this case.
// ChosenPlanStateEnum.stateComplained => throw UnimplementedError(),
// // TODO: Handle this case.
// ChosenPlanStateEnum.stateDone => throw UnimplementedError(),
// // TODO: Handle this case.
// ChosenPlanStateEnum.stateChargeWasCollected => throw UnimplementedError(),
// },
],
),
),
],
),
),
Container(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SizedBox(width: 8),
InkWell(
onTap: () async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.custom,
allowedExtensions: [
'pdf',
'doc',
'docx',
'json',
'ppt',
'pptx',
'rtf',
'txt',
'xls',
'xlsx',
'csv',
'jpeg',
'jpg',
'png',
'gif',
'svg',
'xml',
'webp',
],
);
if (result != null) {
List<File> files = result.paths.map((path) => File(path!)).toList();
await chatRoomCubit.sendMessage(
chatId: widget.chatId,
files: files,
);
} else {
// User canceled the picker
}
},
customBorder: CircleBorder(),
child: Ink(
height: 40,
width: 40,
child: Transform.rotate(
angle: pi / 4,
child: Icon(
Icons.attach_file_rounded,
size: 30,
color: Color(0xFF858E99),
),
),
),
),
SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 40,
child: TextFormField(
expands: true,
controller: textEditingController,
maxLines: null,
minLines: null,
keyboardType: TextInputType.multiline,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 10),
hintText: 'Сообщение',
hintStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1,
color: Color(0xFFAEAEB2),
),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: BorderSide(
color: Color(0xFFD1D1D6),
width: 1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: BorderSide(
color: Color(0xFFD1D1D6),
width: 1,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: BorderSide(
color: Color(0xFFD1D1D6),
width: 1,
),
),
),
),
),
),
SizedBox(width: 8),
InkWell(
customBorder: CircleBorder(),
onTap: () {
if (textEditingController.text.isNotEmpty) {
chatRoomCubit.sendMessage(
chatId: widget.chatId,
data: textEditingController.text.trim(),
);
setState(() {
textEditingController.clear();
});
}
},
child: Ink(
decoration: BoxDecoration(
color: appTheme.blueA400,
shape: BoxShape.circle,
),
height: 40,
width: 40,
child: Icon(
Icons.arrow_upward_rounded,
size: 24,
color: Colors.white,
),
),
),
SizedBox(width: 8),
],
),
),
],
),
if (showAboutOrderButton)
Positioned(
top: 8,
right: 15,
left: 16,
child: GestureDetector(
onTap: () {},
child: Container(
height: 44,
decoration: BoxDecoration(
color: appTheme.gray50,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox.shrink(),
Text(
S.current.more_about_order,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Container(
margin: EdgeInsets.only(right: 4),
width: 36,
height: 36,
decoration: BoxDecoration(
color: appTheme.whiteA700,
shape: BoxShape.circle,
),
child: Center(
child: SvgPicture.asset(
'assets/icons/right_arrow.svg',
height: 14,
width: 8,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
),
],
),
),
),
),
),
Container(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SizedBox(width: 8),
InkWell(
onTap: () async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.custom,
allowedExtensions: [
'pdf',
'doc',
'docx',
'json',
'ppt',
'pptx',
'rtf',
'txt',
'xls',
'xlsx',
'csv',
'jpeg',
'jpg',
'png',
'gif',
'svg',
'xml',
'webp',
],
);
if (result != null) {
List<File> files = result.paths.map((path) => File(path!)).toList();
await chatRoomCubit.sendMessage(
chatId: widget.chatId,
files: files,
);
} else {
// User canceled the picker
}
},
customBorder: CircleBorder(),
child: Ink(
height: 40,
width: 40,
child: Transform.rotate(
angle: pi / 4,
child: Icon(
Icons.attach_file_rounded,
size: 30,
color: Color(0xFF858E99),
),
),
),
),
SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 40,
child: TextFormField(
expands: true,
controller: textEditingController,
maxLines: null,
minLines: null,
keyboardType: TextInputType.multiline,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 10),
hintText: 'Сообщение',
hintStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1,
color: Color(0xFFAEAEB2),
),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: BorderSide(
color: Color(0xFFD1D1D6),
width: 1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: BorderSide(
color: Color(0xFFD1D1D6),
width: 1,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: BorderSide(
color: Color(0xFFD1D1D6),
width: 1,
),
),
),
),
),
),
SizedBox(width: 8),
InkWell(
customBorder: CircleBorder(),
onTap: () {
if (textEditingController.text.isNotEmpty) {
chatRoomCubit.sendMessage(
chatId: widget.chatId,
data: textEditingController.text.trim(),
);
setState(() {
textEditingController.clear();
});
}
},
child: Ink(
decoration: BoxDecoration(
color: appTheme.blueA400,
shape: BoxShape.circle,
),
height: 40,
width: 40,
child: Icon(
Icons.arrow_upward_rounded,
size: 24,
color: Colors.white,
),
),
),
SizedBox(width: 8),
],
),
),
],
),
),

View File

@@ -1,18 +1,136 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
import 'package:eatvio_app/core/utils/string_extentions.dart';
import 'package:eatvio_app/core/widgets/item_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
class OrderCard extends StatelessWidget {
class OrderCard extends StatefulWidget {
final PlanModel plan;
final String optionType;
final bool isUserView;
const OrderCard({super.key, required this.plan, required this.isUserView});
final Future<void> Function() onCancel;
const OrderCard({super.key, required this.plan, required this.isUserView, required this.optionType, required this.onCancel});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(top: 8, bottom: 12, left: 8, right: 8),
child: Column(
children: [
State<OrderCard> createState() => _OrderCardState();
}
class _OrderCardState extends State<OrderCard> {
bool isLoading = false;
@override
Widget build(BuildContext context) {
final optionIndex = int.parse(widget.optionType.characters.last);
final option = switch (optionIndex) {
1 => widget.plan.packet1Options,
2 => widget.plan.packet2Options,
3 => widget.plan.packet3Options,
_ => null,
};
final s = S.of(context);
return Container(
margin: EdgeInsets.symmetric(horizontal: 16),
padding: EdgeInsets.only(top: 8, bottom: 12, left: 8, right: 8),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.gray60004),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 200,
width: double.infinity,
child: Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
baseUrl + '/storage/' + widget.plan.image,
width: double.infinity,
fit: BoxFit.cover,
),
),
Positioned(
left: 0,
bottom: 44,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: appTheme.blueA400,
borderRadius: BorderRadius.only(
topRight: Radius.circular(12),
bottomRight: Radius.circular(12),
),
),
child: Text(
'${widget.plan.duration} ${getDayForm(widget.plan.duration, s).capitalize()}',
style: TextStyles.textSMedium.copyWith(color: appTheme.whiteA700),
),
),
),
Positioned(
left: 0,
bottom: 12,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: appTheme.greenA700,
borderRadius: BorderRadius.only(
topRight: Radius.circular(12),
bottomRight: Radius.circular(12),
),
),
child: Text(
'${s.from} ${widget.plan.minPriceOfOptions.toStringAsFixed(0)}₽',
style: TextStyles.textSMedium.copyWith(color: appTheme.whiteA700),
),
),
),
],
),
),
SizedBox(height: 12),
Text(
'${widget.plan.name}',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
SizedBox(height: 12),
Text(
'${s.option}: ${option?.short ?? ''}',
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
SizedBox(height: 8),
if (option != null)
...option.items
.map(
(element) => Padding(
padding: const EdgeInsets.only(bottom: 4),
child: ItemWidget(
reversed: true,
iconSize: 12,
text: element,
),
),
)
.toList(),
if (widget.isUserView) ...[
SizedBox(height: 8),
isLoading
? Loader()
: GestureDetector(
onTap: () async {
setState(() => isLoading = true);
await widget.onCancel();
setState(() => isLoading = false);
},
child: Text(
'${s.cancel_order}',
style: TextStyles.textSMedium.copyWith(color: appTheme.strongRed),
),
),
],
],
),
);