447 lines
18 KiB
Dart
447 lines
18 KiB
Dart
// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
||
|
||
import 'dart:io';
|
||
|
||
import 'package:any_link_preview/any_link_preview.dart';
|
||
import 'package:audioplayers/audioplayers.dart';
|
||
import 'package:dio/dio.dart';
|
||
import 'package:ebroker/Ui/screens/chat/chat_screen.dart';
|
||
import 'package:ebroker/app/app_theme.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/gestures.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:open_filex/open_filex.dart';
|
||
import 'package:path_provider/path_provider.dart';
|
||
import 'package:permission_handler/permission_handler.dart';
|
||
import 'package:url_launcher/url_launcher.dart';
|
||
|
||
import '../../../../../data/cubits/chatCubits/send_message.dart';
|
||
import '../../../../../data/cubits/system/app_theme_cubit.dart';
|
||
import '../../../../../utils/Extensions/extensions.dart';
|
||
import '../../../../../utils/Notification/chat_message_handler.dart';
|
||
import '../../../../../utils/helper_utils.dart';
|
||
import '../../../../../utils/hive_utils.dart';
|
||
import '../../../../../utils/ui_utils.dart';
|
||
|
||
part "parts/attachment.part.dart";
|
||
part "parts/linkpreview.part.dart";
|
||
part "parts/recordmsg.part.dart";
|
||
|
||
////Please don't make chaneges without sufficent knowledege in this file. otherwise you will be responsable for it
|
||
///
|
||
//This will store and ensure that msg is already sent so we don't have to send it again
|
||
Set sentMessages = {};
|
||
|
||
class ChatMessage extends StatefulWidget {
|
||
final String message;
|
||
final String senderId;
|
||
final bool isSentByMe;
|
||
final bool? isSentNow;
|
||
final String propertyId;
|
||
final String reciverId;
|
||
final bool isChatAudio;
|
||
final bool hasAttachment;
|
||
final dynamic audioFile;
|
||
final String time;
|
||
final dynamic attachment;
|
||
final Function(int id)? onHold;
|
||
|
||
const ChatMessage(
|
||
{super.key,
|
||
this.isSentNow,
|
||
required this.message,
|
||
required this.isSentByMe,
|
||
required this.isChatAudio,
|
||
this.audioFile,
|
||
this.attachment,
|
||
required this.senderId,
|
||
required this.time,
|
||
required this.hasAttachment,
|
||
required this.propertyId,
|
||
required this.reciverId,
|
||
this.onHold});
|
||
|
||
@override
|
||
State<ChatMessage> createState() => ChatMessageState();
|
||
|
||
Map toMap() {
|
||
Map data = {};
|
||
data['key'] = key;
|
||
data['message'] = message;
|
||
data['isSentNow'] = isSentNow;
|
||
data['isSentByMe'] = isSentByMe;
|
||
data['isChatAudio'] = isChatAudio;
|
||
data['senderId'] = senderId;
|
||
data['propertyId'] = propertyId;
|
||
data['reciverId'] = reciverId;
|
||
data['hasAttachment'] = hasAttachment;
|
||
data['audioFile'] = audioFile;
|
||
data['time'] = time;
|
||
data['attachment'] = attachment;
|
||
return data;
|
||
}
|
||
|
||
factory ChatMessage.fromMap(Map json) {
|
||
var chat = ChatMessage(
|
||
key: json['key'],
|
||
message: json['message'],
|
||
isSentByMe: json['isSentByMe'],
|
||
isChatAudio: json['isChatAudio'],
|
||
senderId: json['senderId'],
|
||
audioFile: json['audioFile'],
|
||
attachment: json['attachment'],
|
||
time: json['time'],
|
||
hasAttachment: json['hasAttachment'],
|
||
propertyId: json['propertyId'],
|
||
reciverId: json['reciverId']);
|
||
return chat;
|
||
}
|
||
}
|
||
|
||
class ChatMessageState extends State<ChatMessage>
|
||
with AutomaticKeepAliveClientMixin {
|
||
bool isChatSent = false;
|
||
bool selectedMessage = false;
|
||
static bool isMounted = false;
|
||
String? link;
|
||
final ValueNotifier _linkAddNotifier = ValueNotifier("");
|
||
@override
|
||
void initState() {
|
||
///isSentNow is for check if we are not appending messages multiple time
|
||
if (widget.isSentByMe &&
|
||
(widget.isSentNow == true) &&
|
||
isChatSent == false) {
|
||
if (!sentMessages.contains(widget.key)) {
|
||
context.read<SendMessageCubit>().send(
|
||
senderId: HiveUtils.getUserId().toString(),
|
||
recieverId: widget.reciverId,
|
||
attachment: widget.attachment,
|
||
message: widget.message,
|
||
proeprtyId: widget.propertyId,
|
||
audio: widget.audioFile,
|
||
);
|
||
}
|
||
sentMessages.add(widget.key);
|
||
|
||
isMounted = true;
|
||
}
|
||
|
||
super.initState();
|
||
}
|
||
|
||
String _emptyTextIfAttachmentHasNoText() {
|
||
if (widget.hasAttachment) {
|
||
if (widget.message == "[File]") {
|
||
return "";
|
||
} else {
|
||
return widget.message;
|
||
}
|
||
} else {
|
||
return widget.message;
|
||
}
|
||
}
|
||
|
||
bool _isLink(String input) {
|
||
///This will check if text contains link
|
||
final matcher = RegExp(
|
||
r"(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)");
|
||
return matcher.hasMatch(input);
|
||
}
|
||
|
||
List _replaceLink() {
|
||
//This function will make part of text where link starts. we put invisible charector so we can split it with it
|
||
final linkPattern = RegExp(
|
||
r"(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)");
|
||
|
||
///This is invisible charector [You can replace it with any special chareactor which generally nobody use]
|
||
const String substringIdentifier = "";
|
||
|
||
///This will find and add invisible charector in prefix and suffix
|
||
String splitMapJoin = _emptyTextIfAttachmentHasNoText().splitMapJoin(
|
||
linkPattern,
|
||
onMatch: (match) {
|
||
return substringIdentifier + match.group(0)! + substringIdentifier;
|
||
},
|
||
onNonMatch: (match) {
|
||
return match;
|
||
},
|
||
);
|
||
//finally we split it with invisible charector so it will become list
|
||
return splitMapJoin.split(substringIdentifier);
|
||
}
|
||
|
||
List<String> _matchAstric(String data) {
|
||
var pattern = RegExp(r"\*(.*?)\*");
|
||
|
||
String mapJoin = data.splitMapJoin(
|
||
pattern,
|
||
onMatch: (p0) {
|
||
return "${p0.group(0)!}";
|
||
},
|
||
onNonMatch: (p0) {
|
||
return p0;
|
||
},
|
||
);
|
||
|
||
return mapJoin.split("");
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
super.build(context);
|
||
|
||
bool isDark =
|
||
context.watch<AppThemeCubit>().state.appTheme == AppTheme.dark;
|
||
|
||
return GestureDetector(
|
||
onLongPress: () {
|
||
selectedMessageid.value = (widget.key as ValueKey).value;
|
||
selectedRecieverId.value = int.parse(widget.reciverId);
|
||
showDeletebutton.value = true;
|
||
},
|
||
onTap: () {
|
||
selectedMessage = false;
|
||
},
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 7),
|
||
child: Container(
|
||
alignment:
|
||
widget.isSentByMe ? Alignment.centerRight : Alignment.centerLeft,
|
||
width: MediaQuery.of(context).size.width,
|
||
margin: EdgeInsets.only(
|
||
// top: MediaQuery.of(context).size.height * 0.007,
|
||
right: widget.isSentByMe ? 20 : 0,
|
||
left: widget.isSentByMe ? 0 : 20,
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: widget.isSentByMe
|
||
? CrossAxisAlignment.end
|
||
: CrossAxisAlignment.start,
|
||
children: [
|
||
Container(
|
||
constraints:
|
||
BoxConstraints(maxWidth: context.screenWidth * 0.74),
|
||
decoration: BoxDecoration(
|
||
color: selectedMessage == true
|
||
? (widget.isSentByMe == true
|
||
? context.color.tertiaryColor.darken(45)
|
||
: context.color.secondaryColor.darken(45))
|
||
: (widget.isSentByMe
|
||
? const Color(0xffEEEEEE)
|
||
: context.color.secondaryColor),
|
||
borderRadius: BorderRadius.circular(8)
|
||
|
||
// BorderRadius.only(
|
||
// topRight: widget.isSentByMe
|
||
// ? Radius.zero
|
||
// : const Radius.circular(10),
|
||
// topLeft: widget.isSentByMe
|
||
// ? const Radius.circular(10)
|
||
// : Radius.zero,
|
||
// bottomLeft: const Radius.circular(10),
|
||
// bottomRight: const Radius.circular(10),
|
||
// ),
|
||
),
|
||
child: Wrap(
|
||
runAlignment: WrapAlignment.end,
|
||
alignment: WrapAlignment.end,
|
||
crossAxisAlignment: WrapCrossAlignment.end,
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.all(12),
|
||
child: Container(
|
||
child: widget.isChatAudio
|
||
? RecordMessage(
|
||
url: widget.audioFile ?? "",
|
||
isSentByMe: widget.isSentByMe,
|
||
)
|
||
: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
if (widget.hasAttachment)
|
||
AttachmentMessage(
|
||
url: widget.attachment,
|
||
isSentByMe: widget.isSentByMe),
|
||
|
||
//This is preview builder for image
|
||
ValueListenableBuilder(
|
||
valueListenable: _linkAddNotifier,
|
||
builder: (context, dynamic value, c) {
|
||
if (value == null) {
|
||
return const SizedBox.shrink();
|
||
}
|
||
|
||
return FutureBuilder(
|
||
future: AnyLinkPreview.getMetadata(
|
||
link: value),
|
||
builder: (context,
|
||
AsyncSnapshot snapshot) {
|
||
if (snapshot.connectionState ==
|
||
ConnectionState.done) {
|
||
if (snapshot.data == null) {
|
||
return const SizedBox.shrink();
|
||
}
|
||
return LinkPreviw(
|
||
snapshot: snapshot,
|
||
link: value,
|
||
);
|
||
}
|
||
return const SizedBox.shrink();
|
||
},
|
||
);
|
||
}),
|
||
SelectableText.rich(
|
||
TextSpan(
|
||
style: TextStyle(
|
||
color: (isDark && !widget.isSentByMe)
|
||
? context.color.buttonColor
|
||
: Colors.black),
|
||
children: _replaceLink().map((data) {
|
||
//This will add link to msg
|
||
if (_isLink(data)) {
|
||
//This will notify priview object that it has link
|
||
_linkAddNotifier.value = data;
|
||
_linkAddNotifier.notifyListeners();
|
||
|
||
return TextSpan(
|
||
text: data,
|
||
recognizer: TapGestureRecognizer()
|
||
..onTap = () async {
|
||
await launchUrl(
|
||
Uri.parse(data));
|
||
},
|
||
style: TextStyle(
|
||
decoration:
|
||
TextDecoration.underline,
|
||
color: Colors.blue[800]));
|
||
}
|
||
//This will make text bold
|
||
return TextSpan(
|
||
text: "",
|
||
children:
|
||
_matchAstric(data).map((text) {
|
||
if (text
|
||
.toString()
|
||
.startsWith("*") &&
|
||
text.toString().endsWith("*")) {
|
||
return TextSpan(
|
||
text:
|
||
text.replaceAll("*", ""),
|
||
style: TextStyle(
|
||
color: (isDark &&
|
||
!widget
|
||
.isSentByMe)
|
||
? context
|
||
.color.buttonColor
|
||
: Colors.black,
|
||
fontWeight:
|
||
FontWeight.w800));
|
||
}
|
||
|
||
return TextSpan(
|
||
text: text,
|
||
style: TextStyle(
|
||
color: (isDark &&
|
||
!widget.isSentByMe)
|
||
? context
|
||
.color.buttonColor
|
||
: Colors.black));
|
||
}).toList(),
|
||
style: TextStyle(
|
||
color: widget.isSentByMe
|
||
? context.color.secondaryColor
|
||
: context
|
||
.color.textColorDark),
|
||
);
|
||
}).toList(),
|
||
),
|
||
style: TextStyle(
|
||
color: (isDark && !widget.isSentByMe)
|
||
? context.color.buttonColor
|
||
: Colors.black),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
if (widget.isSentByMe && widget.isSentNow == true) ...[
|
||
BlocConsumer<SendMessageCubit, SendMessageState>(
|
||
listener: (context, state) {
|
||
if (state is SendMessageSuccess) {
|
||
isChatSent = true;
|
||
|
||
///Value which we added locally
|
||
ValueKey? uniqueIdentifier = widget.key as ValueKey;
|
||
|
||
////We were added local id so whenit completed we will replace it with server message id
|
||
|
||
ChatMessageHandlerOLD.updateMessageId(
|
||
uniqueIdentifier.value, state.messageId);
|
||
|
||
WidgetsBinding.instance
|
||
.addPostFrameCallback((timeStamp) {
|
||
if (mounted) setState(() {});
|
||
});
|
||
}
|
||
},
|
||
builder: (context, state) {
|
||
if (state is SendMessageInProgress) {
|
||
return Padding(
|
||
padding:
|
||
const EdgeInsets.only(right: 5.0, bottom: 2),
|
||
child: Icon(
|
||
Icons.watch_later_outlined,
|
||
size: context.font.smaller,
|
||
color: Colors.black,
|
||
),
|
||
);
|
||
}
|
||
|
||
if (state is SendMessageFailed) {
|
||
return Padding(
|
||
padding:
|
||
const EdgeInsets.only(right: 5.0, bottom: 2),
|
||
child: Icon(
|
||
Icons.error,
|
||
size: context.font.smaller,
|
||
color: Colors.black,
|
||
),
|
||
);
|
||
}
|
||
return const SizedBox.shrink();
|
||
},
|
||
)
|
||
]
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(
|
||
height: 5,
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.only(right: 3.0),
|
||
child: Text(
|
||
(DateTime.parse(widget.time))
|
||
.toLocal()
|
||
.toIso8601String()
|
||
.toString()
|
||
.formatDate(format: "hh:mm aa"), //
|
||
style: TextStyle(
|
||
color: widget.isSentByMe
|
||
? context.color.textLightColor
|
||
: context.color.textLightColor),
|
||
).size(context.font.smaller),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
bool get wantKeepAlive => true;
|
||
}
|