feat(chat): add init and dispose chat, typing info, receive a messages

This commit is contained in:
alex
2024-07-03 16:50:09 +03:00
parent 424ce3cb34
commit 4c7519ee4c
27 changed files with 553 additions and 167 deletions

View File

@@ -1,20 +1,108 @@
import 'dart:convert';
import 'package:dart_pusher_channels/dart_pusher_channels.dart';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/initialization_cubit/initialization_cubit.dart';
import 'package:eatvio_app/core/initialization_cubit/initialization_state.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart';
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/locator_service.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ChatRoomCubit extends Cubit<ChatRoomState> {
final ChatRepository chatRepository;
late final PusherChannelsClient client;
late final PrivateChannel privateChannel;
ChatRoomCubit({
required this.chatRepository,
}) : super(ChatRoomState.loading());
initChat(String chatId) async {
try {
final chatRoomModel = await chatRepository.getChatRoom(chatId);
emit(ChatRoomState.loaded(chatRoomModel: chatRoomModel));
final chatRoom = await chatRepository.getChatRoom(chatId);
final chatMessages = await chatRepository.getChatMessages(chatId);
final initCubit = sl<InitializationCubit>();
if (initCubit.state is InitializationStateSuccess) {
await subscribeToChatChannel(initCubit: initCubit, chatId: chatId);
} else {
await initCubit.runInitialization();
await subscribeToChatChannel(initCubit: initCubit, chatId: chatId);
}
emit(ChatRoomState.loaded(
chatRoom: chatRoom,
chatMessages: chatMessages,
isTyping: false,
));
} catch (e) {
emit(ChatRoomState.error(errorMessage: 'Ошибка получеия данных'));
}
}
Future<void> subscribeToChatChannel({
required InitializationCubit initCubit,
required String chatId,
}) async {
client = (initCubit.state as InitializationStateSuccess).client;
privateChannel = await client.privateChannel(
'private-direct-chat.$chatId',
authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel(
authorizationEndpoint: Uri.parse('https://mobile-stage.eatvio.ru/broadcasting/auth'),
headers: {
'Authorization': 'Bearer ${await TokenStorage.getTokenFromStorage()}',
},
),
);
privateChannel.subscribe();
privateChannel.bind('App\\Events\\ChatMessageUpdated').listen((event) {
List<ChatMessageModel> messagesList = [
ChatMessageModel.fromJson(jsonDecode(event.data)['message']),
...(state as ChatRoomStateLoaded).chatMessages.messagesList,
];
emit(
ChatRoomState.loaded(
chatRoom: (state as ChatRoomStateLoaded).chatRoom,
chatMessages: (state as ChatRoomStateLoaded).chatMessages.copyWith(messagesList: messagesList),
isTyping: (state as ChatRoomStateLoaded).isTyping,
),
);
});
privateChannel.bind('client-typing').listen((event) {
emit(
ChatRoomState.loaded(
chatRoom: (state as ChatRoomStateLoaded).chatRoom,
chatMessages: (state as ChatRoomStateLoaded).chatMessages,
isTyping: true,
),
);
EasyDebounce.debounce(
'client-typing',
Duration(seconds: 3),
() => emit(
ChatRoomState.loaded(
chatRoom: (state as ChatRoomStateLoaded).chatRoom,
chatMessages: (state as ChatRoomStateLoaded).chatMessages,
isTyping: false,
),
),
);
});
}
@override
Future<void> close() async {
privateChannel.unsubscribe();
super.close();
}
}

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/features/chat/models/chat_messages_model.dart';
import 'package:eatvio_app/features/chat/models/chat_room_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -10,7 +11,9 @@ abstract class ChatRoomState with _$ChatRoomState {
const factory ChatRoomState.loading() = ChatRoomStateLoading;
const factory ChatRoomState.loaded({
required ChatRoomModel chatRoomModel,
required ChatRoomModel chatRoom,
required ChatMessagesModel chatMessages,
required bool isTyping,
}) = ChatRoomStateLoaded;
const factory ChatRoomState.error({

View File

@@ -20,7 +20,9 @@ mixin _$ChatRoomState {
TResult when<TResult extends Object?>(
TResult Function() $default, {
required TResult Function() loading,
required TResult Function(ChatRoomModel chatRoomModel) loaded,
required TResult Function(ChatRoomModel chatRoom,
ChatMessagesModel chatMessages, bool isTyping)
loaded,
required TResult Function(String errorMessage) error,
}) =>
throw _privateConstructorUsedError;
@@ -28,7 +30,9 @@ mixin _$ChatRoomState {
TResult? whenOrNull<TResult extends Object?>(
TResult? Function()? $default, {
TResult? Function()? loading,
TResult? Function(ChatRoomModel chatRoomModel)? loaded,
TResult? Function(ChatRoomModel chatRoom, ChatMessagesModel chatMessages,
bool isTyping)?
loaded,
TResult? Function(String errorMessage)? error,
}) =>
throw _privateConstructorUsedError;
@@ -36,7 +40,9 @@ mixin _$ChatRoomState {
TResult maybeWhen<TResult extends Object?>(
TResult Function()? $default, {
TResult Function()? loading,
TResult Function(ChatRoomModel chatRoomModel)? loaded,
TResult Function(ChatRoomModel chatRoom, ChatMessagesModel chatMessages,
bool isTyping)?
loaded,
TResult Function(String errorMessage)? error,
required TResult orElse(),
}) =>
@@ -126,7 +132,9 @@ class _$ChatRoomStateImpl implements _ChatRoomState {
TResult when<TResult extends Object?>(
TResult Function() $default, {
required TResult Function() loading,
required TResult Function(ChatRoomModel chatRoomModel) loaded,
required TResult Function(ChatRoomModel chatRoom,
ChatMessagesModel chatMessages, bool isTyping)
loaded,
required TResult Function(String errorMessage) error,
}) {
return $default();
@@ -137,7 +145,9 @@ class _$ChatRoomStateImpl implements _ChatRoomState {
TResult? whenOrNull<TResult extends Object?>(
TResult? Function()? $default, {
TResult? Function()? loading,
TResult? Function(ChatRoomModel chatRoomModel)? loaded,
TResult? Function(ChatRoomModel chatRoom, ChatMessagesModel chatMessages,
bool isTyping)?
loaded,
TResult? Function(String errorMessage)? error,
}) {
return $default?.call();
@@ -148,7 +158,9 @@ class _$ChatRoomStateImpl implements _ChatRoomState {
TResult maybeWhen<TResult extends Object?>(
TResult Function()? $default, {
TResult Function()? loading,
TResult Function(ChatRoomModel chatRoomModel)? loaded,
TResult Function(ChatRoomModel chatRoom, ChatMessagesModel chatMessages,
bool isTyping)?
loaded,
TResult Function(String errorMessage)? error,
required TResult orElse(),
}) {
@@ -241,7 +253,9 @@ class _$ChatRoomStateLoadingImpl implements ChatRoomStateLoading {
TResult when<TResult extends Object?>(
TResult Function() $default, {
required TResult Function() loading,
required TResult Function(ChatRoomModel chatRoomModel) loaded,
required TResult Function(ChatRoomModel chatRoom,
ChatMessagesModel chatMessages, bool isTyping)
loaded,
required TResult Function(String errorMessage) error,
}) {
return loading();
@@ -252,7 +266,9 @@ class _$ChatRoomStateLoadingImpl implements ChatRoomStateLoading {
TResult? whenOrNull<TResult extends Object?>(
TResult? Function()? $default, {
TResult? Function()? loading,
TResult? Function(ChatRoomModel chatRoomModel)? loaded,
TResult? Function(ChatRoomModel chatRoom, ChatMessagesModel chatMessages,
bool isTyping)?
loaded,
TResult? Function(String errorMessage)? error,
}) {
return loading?.call();
@@ -263,7 +279,9 @@ class _$ChatRoomStateLoadingImpl implements ChatRoomStateLoading {
TResult maybeWhen<TResult extends Object?>(
TResult Function()? $default, {
TResult Function()? loading,
TResult Function(ChatRoomModel chatRoomModel)? loaded,
TResult Function(ChatRoomModel chatRoom, ChatMessagesModel chatMessages,
bool isTyping)?
loaded,
TResult Function(String errorMessage)? error,
required TResult orElse(),
}) {
@@ -321,9 +339,11 @@ abstract class _$$ChatRoomStateLoadedImplCopyWith<$Res> {
$Res Function(_$ChatRoomStateLoadedImpl) then) =
__$$ChatRoomStateLoadedImplCopyWithImpl<$Res>;
@useResult
$Res call({ChatRoomModel chatRoomModel});
$Res call(
{ChatRoomModel chatRoom, ChatMessagesModel chatMessages, bool isTyping});
$ChatRoomModelCopyWith<$Res> get chatRoomModel;
$ChatRoomModelCopyWith<$Res> get chatRoom;
$ChatMessagesModelCopyWith<$Res> get chatMessages;
}
/// @nodoc
@@ -337,21 +357,39 @@ class __$$ChatRoomStateLoadedImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? chatRoomModel = null,
Object? chatRoom = null,
Object? chatMessages = null,
Object? isTyping = null,
}) {
return _then(_$ChatRoomStateLoadedImpl(
chatRoomModel: null == chatRoomModel
? _value.chatRoomModel
: chatRoomModel // ignore: cast_nullable_to_non_nullable
chatRoom: null == chatRoom
? _value.chatRoom
: chatRoom // ignore: cast_nullable_to_non_nullable
as ChatRoomModel,
chatMessages: null == chatMessages
? _value.chatMessages
: chatMessages // ignore: cast_nullable_to_non_nullable
as ChatMessagesModel,
isTyping: null == isTyping
? _value.isTyping
: isTyping // ignore: cast_nullable_to_non_nullable
as bool,
));
}
@override
@pragma('vm:prefer-inline')
$ChatRoomModelCopyWith<$Res> get chatRoomModel {
return $ChatRoomModelCopyWith<$Res>(_value.chatRoomModel, (value) {
return _then(_value.copyWith(chatRoomModel: value));
$ChatRoomModelCopyWith<$Res> get chatRoom {
return $ChatRoomModelCopyWith<$Res>(_value.chatRoom, (value) {
return _then(_value.copyWith(chatRoom: value));
});
}
@override
@pragma('vm:prefer-inline')
$ChatMessagesModelCopyWith<$Res> get chatMessages {
return $ChatMessagesModelCopyWith<$Res>(_value.chatMessages, (value) {
return _then(_value.copyWith(chatMessages: value));
});
}
}
@@ -359,14 +397,21 @@ class __$$ChatRoomStateLoadedImplCopyWithImpl<$Res>
/// @nodoc
class _$ChatRoomStateLoadedImpl implements ChatRoomStateLoaded {
const _$ChatRoomStateLoadedImpl({required this.chatRoomModel});
const _$ChatRoomStateLoadedImpl(
{required this.chatRoom,
required this.chatMessages,
required this.isTyping});
@override
final ChatRoomModel chatRoomModel;
final ChatRoomModel chatRoom;
@override
final ChatMessagesModel chatMessages;
@override
final bool isTyping;
@override
String toString() {
return 'ChatRoomState.loaded(chatRoomModel: $chatRoomModel)';
return 'ChatRoomState.loaded(chatRoom: $chatRoom, chatMessages: $chatMessages, isTyping: $isTyping)';
}
@override
@@ -374,12 +419,17 @@ class _$ChatRoomStateLoadedImpl implements ChatRoomStateLoaded {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ChatRoomStateLoadedImpl &&
(identical(other.chatRoomModel, chatRoomModel) ||
other.chatRoomModel == chatRoomModel));
(identical(other.chatRoom, chatRoom) ||
other.chatRoom == chatRoom) &&
(identical(other.chatMessages, chatMessages) ||
other.chatMessages == chatMessages) &&
(identical(other.isTyping, isTyping) ||
other.isTyping == isTyping));
}
@override
int get hashCode => Object.hash(runtimeType, chatRoomModel);
int get hashCode =>
Object.hash(runtimeType, chatRoom, chatMessages, isTyping);
@JsonKey(ignore: true)
@override
@@ -393,10 +443,12 @@ class _$ChatRoomStateLoadedImpl implements ChatRoomStateLoaded {
TResult when<TResult extends Object?>(
TResult Function() $default, {
required TResult Function() loading,
required TResult Function(ChatRoomModel chatRoomModel) loaded,
required TResult Function(ChatRoomModel chatRoom,
ChatMessagesModel chatMessages, bool isTyping)
loaded,
required TResult Function(String errorMessage) error,
}) {
return loaded(chatRoomModel);
return loaded(chatRoom, chatMessages, isTyping);
}
@override
@@ -404,10 +456,12 @@ class _$ChatRoomStateLoadedImpl implements ChatRoomStateLoaded {
TResult? whenOrNull<TResult extends Object?>(
TResult? Function()? $default, {
TResult? Function()? loading,
TResult? Function(ChatRoomModel chatRoomModel)? loaded,
TResult? Function(ChatRoomModel chatRoom, ChatMessagesModel chatMessages,
bool isTyping)?
loaded,
TResult? Function(String errorMessage)? error,
}) {
return loaded?.call(chatRoomModel);
return loaded?.call(chatRoom, chatMessages, isTyping);
}
@override
@@ -415,12 +469,14 @@ class _$ChatRoomStateLoadedImpl implements ChatRoomStateLoaded {
TResult maybeWhen<TResult extends Object?>(
TResult Function()? $default, {
TResult Function()? loading,
TResult Function(ChatRoomModel chatRoomModel)? loaded,
TResult Function(ChatRoomModel chatRoom, ChatMessagesModel chatMessages,
bool isTyping)?
loaded,
TResult Function(String errorMessage)? error,
required TResult orElse(),
}) {
if (loaded != null) {
return loaded(chatRoomModel);
return loaded(chatRoom, chatMessages, isTyping);
}
return orElse();
}
@@ -465,9 +521,13 @@ class _$ChatRoomStateLoadedImpl implements ChatRoomStateLoaded {
abstract class ChatRoomStateLoaded implements ChatRoomState {
const factory ChatRoomStateLoaded(
{required final ChatRoomModel chatRoomModel}) = _$ChatRoomStateLoadedImpl;
{required final ChatRoomModel chatRoom,
required final ChatMessagesModel chatMessages,
required final bool isTyping}) = _$ChatRoomStateLoadedImpl;
ChatRoomModel get chatRoomModel;
ChatRoomModel get chatRoom;
ChatMessagesModel get chatMessages;
bool get isTyping;
@JsonKey(ignore: true)
_$$ChatRoomStateLoadedImplCopyWith<_$ChatRoomStateLoadedImpl> get copyWith =>
throw _privateConstructorUsedError;
@@ -541,7 +601,9 @@ class _$ChatRoomStateErrorImpl implements ChatRoomStateError {
TResult when<TResult extends Object?>(
TResult Function() $default, {
required TResult Function() loading,
required TResult Function(ChatRoomModel chatRoomModel) loaded,
required TResult Function(ChatRoomModel chatRoom,
ChatMessagesModel chatMessages, bool isTyping)
loaded,
required TResult Function(String errorMessage) error,
}) {
return error(errorMessage);
@@ -552,7 +614,9 @@ class _$ChatRoomStateErrorImpl implements ChatRoomStateError {
TResult? whenOrNull<TResult extends Object?>(
TResult? Function()? $default, {
TResult? Function()? loading,
TResult? Function(ChatRoomModel chatRoomModel)? loaded,
TResult? Function(ChatRoomModel chatRoom, ChatMessagesModel chatMessages,
bool isTyping)?
loaded,
TResult? Function(String errorMessage)? error,
}) {
return error?.call(errorMessage);
@@ -563,7 +627,9 @@ class _$ChatRoomStateErrorImpl implements ChatRoomStateError {
TResult maybeWhen<TResult extends Object?>(
TResult Function()? $default, {
TResult Function()? loading,
TResult Function(ChatRoomModel chatRoomModel)? loaded,
TResult Function(ChatRoomModel chatRoom, ChatMessagesModel chatMessages,
bool isTyping)?
loaded,
TResult Function(String errorMessage)? error,
required TResult orElse(),
}) {

View File

@@ -19,6 +19,8 @@ class ChatScreen extends StatefulWidget {
}
class _ChatScreenState extends State<ChatScreen> {
final scrollController = ScrollController();
Widget build(BuildContext context) {
return BlocProvider<ChatRoomCubit>(
create: (BuildContext context) => sl()..initChat(widget.chatId),
@@ -33,7 +35,12 @@ class _ChatScreenState extends State<ChatScreen> {
} else if (state is ChatRoomStateLoaded) {
return _buildChat(context, state);
} else if (state is ChatRoomStateError) {
return Center(child: Text(state.errorMessage));
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text(state.errorMessage),
),
);
} else {
return Scaffold(
body: Center(
@@ -46,6 +53,16 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
void setupScrollController(BuildContext context) {
scrollController.addListener(() {
if (scrollController.position.atEdge) {
if (scrollController.position.pixels != 0) {
context.read<ChatRoomCubit>();
}
}
});
}
_buildChat(BuildContext context, ChatRoomStateLoaded state) {
mediaQueryData = MediaQuery.of(context);
return Scaffold(
@@ -68,7 +85,7 @@ class _ChatScreenState extends State<ChatScreen> {
shape: BoxShape.circle,
),
child: Text(
state.chatRoomModel.users['to_chat_user']!.name.split(' ').first[0] + state.chatRoomModel.users['to_chat_user']!.name.split(' ')[1][0],
state.chatRoom.users['to_chat_user']!.name.split(' ').first[0] + state.chatRoom.users['to_chat_user']!.name.split(' ')[1][0],
style: TextStyle(
color: Color(0xFF7F9CF5),
fontWeight: FontWeight.w400,
@@ -77,11 +94,27 @@ class _ChatScreenState extends State<ChatScreen> {
),
),
SizedBox(width: 16),
Text(
state.chatRoomModel.users['to_chat_user']!.name,
style: CustomTextStyles.titleSmallBold.copyWith(
color: PrimaryColors().blueGray800,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
state.chatRoom.users['to_chat_user']!.name,
style: CustomTextStyles.titleSmallBold.copyWith(
color: PrimaryColors().blueGray800,
),
),
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),
),
],
),
],
),
@@ -95,6 +128,8 @@ class _ChatScreenState extends State<ChatScreen> {
color: Color(0xFFF0F2F5),
width: double.maxFinite,
child: CustomScrollView(
physics: BouncingScrollPhysics(),
controller: scrollController,
reverse: true,
slivers: [
SliverPadding(
@@ -102,12 +137,11 @@ class _ChatScreenState extends State<ChatScreen> {
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ChatBubbleWidget(
text: state.chatRoomModel.messages[state.chatRoomModel.messages.length - index - 1].data,
isCurrentUser: state.chatRoomModel.users['to_chat_user']!.id !=
state.chatRoomModel.messages[state.chatRoomModel.messages.length - index - 1].authorId,
mediaData: state.chatRoomModel.messages[state.chatRoomModel.messages.length - index - 1].mediaData,
text: state.chatMessages.messagesList[index].data.trim(),
isCurrentUser: state.chatRoom.users['to_chat_user']!.id != state.chatMessages.messagesList[index].authorId,
mediaData: state.chatMessages.messagesList[index].mediaData ?? [],
),
childCount: state.chatRoomModel.messages.length,
childCount: state.chatMessages.messagesList.length,
),
),
),

View File

@@ -80,7 +80,7 @@ class ChatEntryWidget extends StatelessWidget {
),
SizedBox(height: 6.v),
Text(
chatEntryObj.greetingText!,
chatEntryObj.greetingText!.trim(),
style: CustomTextStyles.bodyMediumBluegray800,
),
],

View File

@@ -11,12 +11,12 @@ class ChatBubbleWidget extends StatelessWidget {
super.key,
required this.text,
required this.isCurrentUser,
this.mediaData,
required this.mediaData,
});
final String text;
final bool isCurrentUser;
final List<dynamic>? mediaData;
final List<dynamic> mediaData;
@override
Widget build(BuildContext context) {
@@ -32,23 +32,24 @@ class ChatBubbleWidget extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (mediaData != null)
if (mediaData.isNotEmpty)
CustomScrollView(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
slivers: [
SliverList.separated(
itemBuilder: (BuildContext context, int index) {
if (!mediaData![index].toString().contains('.png') &&
!mediaData![index].toString().contains('.jpg') &&
!mediaData![index].toString().contains('.jpeg')) {
if (!mediaData[index].toString().contains('.png') &&
!mediaData[index].toString().contains('.jpg') &&
!mediaData[index].toString().contains('.jpeg')) {
return ListTile(
contentPadding: EdgeInsets.zero,
horizontalTitleGap: 8,
onTap: () async {
if (!await launchUrl(Uri.parse(dotenv.env['STORAGE_URL']! + mediaData![index].toString()))) {
throw Exception('Could not launch ${mediaData![index].toString()}');
if (!await launchUrl(Uri.parse(dotenv.env['STORAGE_URL']! + mediaData[index].toString()))) {
throw Exception('Could not launch ${mediaData[index].toString()}');
}
},
leading: Icon(
@@ -60,7 +61,7 @@ class ChatBubbleWidget extends StatelessWidget {
color: Color(0xFF2F80ED),
),
title: Text(
mediaData![index].toString().replaceAll('chat_files/', ''),
mediaData[index].toString().replaceAll('chat_files/', ''),
maxLines: 2,
style: TextStyle(
color: Color(0xFF323F4B),
@@ -74,29 +75,29 @@ class ChatBubbleWidget extends StatelessWidget {
onTap: () {
context.pushTransparentRoute(
PhotoViewScreen(
imageUrls: mediaData!.map((e) => dotenv.env['STORAGE_URL']! + e.toString()).toList(),
imageUrls: mediaData.map((e) => dotenv.env['STORAGE_URL']! + e.toString()).toList(),
index: index,
),
);
},
child: Hero(
tag: dotenv.env['STORAGE_URL']! + '${mediaData![index]}',
tag: dotenv.env['STORAGE_URL']! + '${mediaData[index]}',
transitionOnUserGestures: true,
child: ClipRRect(
borderRadius: BorderRadiusStyle.roundedBorder8,
child: CachedNetworkImage(
imageUrl: dotenv.env['STORAGE_URL']! + '${mediaData![index]}',
imageUrl: dotenv.env['STORAGE_URL']! + '${mediaData[index]}',
),
),
),
);
},
separatorBuilder: (BuildContext context, int index) => const SizedBox(height: 8),
itemCount: mediaData!.length,
itemCount: mediaData.length,
),
],
),
if (mediaData!.length > 0 && text.isNotEmpty) SizedBox(height: 8),
if (mediaData.length > 0 && text.isNotEmpty) SizedBox(height: 8),
if (text.isNotEmpty)
Text(
text,