Files
eatvio_app/lib/features/chat/presentation/pages/chat_screen.dart
2025-03-24 13:58:01 +03:00

460 lines
20 KiB
Dart

import 'dart:io';
import 'dart:math';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart';
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/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
import 'package:eatvio_app/features/user_details/presentation/user_details_screen.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:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:visibility_detector/visibility_detector.dart';
class ChatScreen extends StatefulWidget {
final String chatId;
const ChatScreen({
required this.chatId,
super.key,
});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final scrollController = ScrollController();
final TextEditingController textEditingController = TextEditingController();
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() {
super.initState();
scrollController.addListener(_onScroll);
chatRoomCubit.initChat(widget.chatId);
}
_onScroll() {
if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) {
chatRoomCubit.loadNextPage(widget.chatId);
}
}
@override
void dispose() {
scrollController.dispose();
chatRoomCubit.close();
super.dispose();
}
Widget build(BuildContext context) {
return BlocProvider<ChatRoomCubit>(
create: (BuildContext context) => chatRoomCubit,
child: BlocBuilder<ChatRoomCubit, ChatRoomState>(
bloc: chatRoomCubit,
builder: (context, state) {
if (state is ChatRoomStateLoaded) {
return _buildChat(context, state);
} else if (state is ChatRoomStateError) {
return Scaffold(
appBar: AppBar(),
body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.chatId)),
);
} else {
return Scaffold(
body: const Loader(),
);
}
},
),
);
}
_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(
titleSpacing: 0,
backgroundColor: appTheme.gray100,
centerTitle: false,
title: GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(
'profileScreen',
extra: Author(
authorSlug: state.chatRoom.users.receiver.slug,
authorName: state.chatRoom.users.receiver.name,
authorProfileIcon: state.chatRoom.users.receiver.profilePhotoUrl ?? '',
authorIsMe: false,
),
);
},
child: Row(
children: [
if (state.chatRoom.users.receiver.profilePhotoUrl != null)
CircleAvatar(
radius: 20,
foregroundImage: NetworkImage(state.chatRoom.users.receiver.profilePhotoUrl!),
),
if (state.chatRoom.users.receiver.profilePhotoUrl == null)
Container(
width: 40,
height: 40,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Color(0xFFEBF4FF),
shape: BoxShape.circle,
),
child: Text(
state.chatRoom.users.receiver.name.split(' ').first[0] + state.chatRoom.users.receiver.name.split(' ')[1][0],
style: TextStyle(
color: Color(0xFF7F9CF5),
fontWeight: FontWeight.w400,
fontSize: 20,
),
),
),
SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
state.chatRoom.users.receiver.name,
style: CustomTextStyles.titleSmallBold.copyWith(
color: PrimaryColors().black,
),
),
AnimatedCrossFade(
firstChild: Text(
'Печатает...',
style: CustomTextStyles.bodySmallMulishPrimary,
),
firstCurve: Curves.easeIn,
secondCurve: Curves.easeOut,
secondChild: const SizedBox.shrink(),
crossFadeState: state.isTyping ? CrossFadeState.showFirst : CrossFadeState.showSecond,
duration: Duration(milliseconds: 150),
),
],
),
],
),
),
),
body: SafeArea(
child: Stack(
children: [
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.receiver.id != state.chatMessages[index].authorId;
return Column(
children: [
_messageDate(state.chatMessages[index].updatedAt),
VisibilityDetector(
key: Key(state.chatMessages[index].id.toString()),
onVisibilityChanged: (VisibilityInfo info) {
if (!state.chatMessages[index].isRead && !isCurrentUserAuthor && info.visibleFraction > 0) {
unreadMessagesIdsCopy.add((id: state.chatMessages[index].id, index: index));
EasyDebounce.debounce(
'mark_as_read',
Duration(milliseconds: 500),
() async {
final result = await context.read<ChatRoomCubit>().markMessagesAsRead(state.chatRoom.id, unreadMessagesIdsCopy);
if (!result) return;
context
.read<ChatsCubit>()
.reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIdsCopy.length);
unreadMessagesIdsCopy.clear();
},
);
}
},
child: ChatBubbleWidget(
isRead: state.chatMessages[index].isRead,
sendTime: state.chatMessages[index].updatedAt,
text: state.chatMessages[index].data.trim(),
isCurrentUser: isCurrentUserAuthor,
mediaData: state.chatMessages[index].mediaData ?? [],
),
),
],
);
}
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
return VisibilityDetector(
key: Key(state.chatMessages[index].id.toString()),
onVisibilityChanged: (VisibilityInfo info) {
if (!state.chatMessages[index].isRead && !isCurrentUserAuthor && info.visibleFraction > 0) {
unreadMessagesIds.add((id: state.chatMessages[index].id, index: index));
EasyDebounce.debounce(
'mark_as_read',
Duration(milliseconds: 500),
() async {
final result = await context.read<ChatRoomCubit>().markMessagesAsRead(state.chatRoom.id, unreadMessagesIds);
if (!result) return;
context.read<ChatsCubit>().reduceUnreadMessagesCount(chatId: state.chatRoom.id, count: unreadMessagesIds.length);
unreadMessagesIds.clear();
},
);
}
},
child: ChatBubbleWidget(
isRead: state.chatMessages[index].isRead,
sendTime: state.chatMessages[index].updatedAt,
text: state.chatMessages[index].data.trim(),
isCurrentUser: isCurrentUserAuthor,
mediaData: state.chatMessages[index].mediaData ?? [],
),
);
},
itemCount: state.chatMessages.length + (state.isLoading ? 1 : 0),
),
SizedBox(height: 16),
if (chosenPlan != null) ...[
OrderCard(
plan: chosenPlan.plan,
isCustomerView: !amIConsultant,
optionType: chosenPlan.optionType,
onCancel: () async {
await context.read<ChatRoomCubit>().cancelChosenPlan(chatId: state.chatRoom.id, planId: chosenPlan.id);
},
),
if (amIConsultant) ...[
Padding(
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: CustomOutlinedButton(
text: S.current.user_info,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
onPressed: () async {
GoRouter.of(context).push(UserDetailsScreen.path, extra: state.chatRoom.users.receiver.slug);
},
),
),
],
],
],
),
),
),
),
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,
);
}
},
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,
textCapitalization: TextCapitalization.sentences,
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: AboutOrderButton(),
),
],
),
),
);
}
_messageDate(DateTime date) {
return Center(
child: Padding(
padding: const EdgeInsets.only(top: 32),
child: Text(
DateFormat.yMMMMd('RU').format(
date,
),
style: const TextStyle(
height: 1,
color: Color(0xFF8393A1),
fontWeight: FontWeight.w500,
),
),
),
);
}
}