fist commit ftc staff app clone
This commit is contained in:
20
lib/view/screens/chat/arguments/chat_screen_args.dart
Normal file
20
lib/view/screens/chat/arguments/chat_screen_args.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ftc_mobile_app/models/chat/ChatModel.dart';
|
||||
import 'group_data_args.dart';
|
||||
|
||||
class ChatScreenArgs {
|
||||
final String name;
|
||||
final String profilePicPath;
|
||||
final String otherUserId;
|
||||
final GroupDataArgs? groupData;
|
||||
|
||||
final ValueChanged<ChatModel>? onLastMessageUpdate;
|
||||
|
||||
ChatScreenArgs({
|
||||
required this.name,
|
||||
required this.profilePicPath,
|
||||
required this.otherUserId,
|
||||
this.groupData,
|
||||
this.onLastMessageUpdate,
|
||||
});
|
||||
}
|
||||
13
lib/view/screens/chat/arguments/group_data_args.dart
Normal file
13
lib/view/screens/chat/arguments/group_data_args.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:ftc_mobile_app/models/chat/combined_last_messages_model_class.dart';
|
||||
|
||||
class GroupDataArgs {
|
||||
final String groupId;
|
||||
final List groupMembersIds;
|
||||
final GroupWorkingScheduleTime scheduleTime;
|
||||
|
||||
GroupDataArgs({
|
||||
required this.groupId,
|
||||
required this.groupMembersIds,
|
||||
required this.scheduleTime,
|
||||
});
|
||||
}
|
||||
190
lib/view/screens/chat/chat_screen.dart
Normal file
190
lib/view/screens/chat/chat_screen.dart
Normal file
@@ -0,0 +1,190 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ftc_mobile_app/ftc_mobile_app.dart';
|
||||
import 'package:ftc_mobile_app/models/chat/ChatModel.dart';
|
||||
import 'package:ftc_mobile_app/utilities/extensions/custom_extensions.dart';
|
||||
import 'package:ftc_mobile_app/view/custom_widgets/my_circle_image.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:grouped_list/grouped_list.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'arguments/chat_screen_args.dart';
|
||||
import 'widgets/chat_screen_footer_widget.dart';
|
||||
import 'widgets/message_bubble.dart';
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final ChatScreenArgs args;
|
||||
|
||||
const ChatScreen({Key? key, required this.args}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ChatScreen> createState() => _ChatScreenState();
|
||||
}
|
||||
|
||||
class _ChatScreenState extends State<ChatScreen> {
|
||||
late final ChatScreenController controller;
|
||||
final sepFormatter = DateFormat("dd MMM yyyy");
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
controller = Get.put(ChatScreenController(widget.args));
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomScaffold(
|
||||
backgroundColor: Colors.white,
|
||||
screenKey: controller.screenKey,
|
||||
onScreenTap: controller.removeFocus,
|
||||
showAppBar: true,
|
||||
appBar: _appBar,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(child: messagesList()),
|
||||
Obx(() {
|
||||
return controller.isSocketConnected()
|
||||
? ChatScreenFooterWidget(
|
||||
controller: controller,
|
||||
enabled: controller.isSocketConnected(),
|
||||
)
|
||||
: FrequentFunctions.noWidget;
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
AppBar get _appBar {
|
||||
return AppBar(
|
||||
toolbarHeight: 56.r,
|
||||
leading: IconButton(
|
||||
icon: CustomImageWidget(
|
||||
imagePath: AssetsManager.kBackIcon,
|
||||
height: 11.53.h,
|
||||
width: 8.66.w,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
centerTitle: false,
|
||||
titleSpacing: 0,
|
||||
leadingWidth: 50.r,
|
||||
surfaceTintColor: Colors.white,
|
||||
title: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MyCircleImage(
|
||||
imageSize: 32.r,
|
||||
url: "${WebUrls.baseUrl}${widget.args.profilePicPath}",
|
||||
errorWidget: CustomImageWidget(
|
||||
imagePath: AssetsManager.kPersonMainIcon,
|
||||
imageColor: CustomAppColors.kDarkBlueTextColor,
|
||||
height: 32.r,
|
||||
width: 32.r,
|
||||
),
|
||||
),
|
||||
10.horizontalSpace,
|
||||
CustomTextWidget(
|
||||
text: widget.args.name,
|
||||
isExpanded: false,
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontColor: CustomAppColors.kDarkBlueTextColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget messagesList() {
|
||||
return Obx(() {
|
||||
final messages = controller.messages;
|
||||
return (messages.isEmpty)
|
||||
? FrequentFunctions.noWidget
|
||||
: _messagesList(messages());
|
||||
});
|
||||
}
|
||||
|
||||
Widget _messagesList(List<ChatModel> list) {
|
||||
final now = DateTime.now();
|
||||
|
||||
return GroupedListView<ChatModel, String>(
|
||||
reverse: true,
|
||||
elements: list,
|
||||
padding: REdgeInsets.symmetric(horizontal: 18),
|
||||
order: GroupedListOrder.DESC,
|
||||
groupBy: (message) {
|
||||
final messageDate =
|
||||
DateTime.fromMillisecondsSinceEpoch(message.date ?? 0);
|
||||
return DateFormatter.dateFormatter.format(messageDate);
|
||||
},
|
||||
groupSeparatorBuilder: (String date) {
|
||||
final isToday = (date == DateFormatter.dateFormatter.format(now));
|
||||
return _buildGroupSeparatorWidget(isToday
|
||||
? "Today"
|
||||
: sepFormatter.format(DateFormatter.dateFormatter.parse(date)));
|
||||
},
|
||||
itemBuilder: (_, e) => _buildItemWidget(list.indexOf(e), e),
|
||||
sort: false,
|
||||
separator: 8.verticalSpace,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGroupSeparatorWidget(String susTag) {
|
||||
return Padding(
|
||||
padding: REdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Divider(
|
||||
height: 1,
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
)),
|
||||
Text(
|
||||
susTag,
|
||||
style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500),
|
||||
).addPaddingHorizontal(10),
|
||||
Expanded(
|
||||
child: Divider(
|
||||
height: 1,
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItemWidget(int index, ChatModel message) {
|
||||
final isMyMessage = (message.from?.id == controller.myId);
|
||||
final hasFile = message.filePath.isNotNullOrEmpty();
|
||||
print("filePath: ${message.filePath}");
|
||||
|
||||
return MessageBubble(
|
||||
senderName: message.from?.name ?? "",
|
||||
profilePic: !isMyMessage ? (message.from?.profilePictureUrl ?? "") : "",
|
||||
content: hasFile ? (message.filePath ?? '') : (message.message ?? ''),
|
||||
type: isMyMessage ? MessageType.sent : MessageType.received,
|
||||
contentType: hasFile
|
||||
? (message.fileType == ChatModel.fileTypeLocalPath)
|
||||
? MessageContentType.file
|
||||
: MessageContentType.url
|
||||
: MessageContentType.text,
|
||||
sentMessageColor: CustomAppColors.kSmokeColor,
|
||||
receivedMessageColor: CustomAppColors.kSecondaryColor,
|
||||
messageTime: (message.date == null)
|
||||
? ""
|
||||
: DateTime.fromMillisecondsSinceEpoch(message.date!)
|
||||
.toIso8601String(),
|
||||
state: MessageState.stateNone,
|
||||
status: MessageSeenStatus.seen,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
503
lib/view/screens/chat/controller/chat_screen_controller.dart
Normal file
503
lib/view/screens/chat/controller/chat_screen_controller.dart
Normal file
@@ -0,0 +1,503 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:ftc_mobile_app/controllers/home/inbox_screen_controller.dart';
|
||||
import 'package:ftc_mobile_app/models/chat/all_group_messages_model.dart';
|
||||
import 'package:ftc_mobile_app/models/chat/single_chat.dart';
|
||||
import 'package:ftc_mobile_app/models/profileData/user_data.dart';
|
||||
import 'package:ftc_mobile_app/utilities/extensions/custom_extensions.dart';
|
||||
import 'package:ftc_mobile_app/utilities/frequent_functions.dart';
|
||||
import 'package:ftc_mobile_app/utilities/image_picker_popup.dart';
|
||||
import 'package:ftc_mobile_app/utilities/local_storage_manager/export_local_storage.dart';
|
||||
import 'package:ftc_mobile_app/view/custom_widgets/home/custom_message_dialog.dart';
|
||||
import 'package:ftc_mobile_app/view/screens/chat/arguments/chat_screen_args.dart';
|
||||
import 'package:ftc_mobile_app/web_services/chat_services.dart';
|
||||
import 'package:ftc_mobile_app/web_services/web_url.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:socket_io_client/socket_io_client.dart';
|
||||
import '../../../../models/chat/ChatModel.dart';
|
||||
|
||||
class ChatScreenController extends GetxController {
|
||||
final GlobalKey<ScaffoldState> screenKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
final messageTEC = TextEditingController();
|
||||
final messageFN = FocusNode();
|
||||
|
||||
final messages = RxList<ChatModel>();
|
||||
final isSocketConnected = false.obs;
|
||||
|
||||
late final ChatScreenArgs args;
|
||||
|
||||
String myId = "";
|
||||
|
||||
ChatScreenController(ChatScreenArgs data) {
|
||||
args = data;
|
||||
}
|
||||
|
||||
late Socket _socketIO;
|
||||
String listenId = "";
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
//Getting my ID
|
||||
// String userJson = LocalStorageManager.getSessionToken(
|
||||
// tokenKey: LocalStorageKeys.kUserModelKey,
|
||||
// );
|
||||
// UserModel userModel = UserModel.fromJson(json.decode(userJson));
|
||||
myId = LocalStorageManager.userId;
|
||||
|
||||
if (_canChat) {
|
||||
initializeSocket();
|
||||
}
|
||||
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
if (isGroup) {
|
||||
fetchGroupMessagesFromService(args.groupData!.groupId);
|
||||
} else {
|
||||
fetchSingleMessagesFromService();
|
||||
}
|
||||
|
||||
if (_canChat.not) {
|
||||
showCantMessageDialog();
|
||||
}
|
||||
super.onReady();
|
||||
}
|
||||
|
||||
bool get isGroup => args.groupData != null;
|
||||
|
||||
/// This method checks the current date if it lies within schedule time or not.
|
||||
/// If it does, it means chat is enabled else chat will be disabled.
|
||||
/// If schedule times not available, it returns true;
|
||||
bool get _canChat {
|
||||
final startMills = args.groupData?.scheduleTime.startTime ?? 0;
|
||||
final endMills = args.groupData?.scheduleTime.endTime ?? 0;
|
||||
|
||||
if (startMills > 0 && endMills > 0) {
|
||||
// Schedule times are available
|
||||
|
||||
final scheduleTime = _getScheduleTime(startMills, endMills);
|
||||
final currentTime = TimeOfDay.now();
|
||||
|
||||
// Current time within start, end schedule time
|
||||
return (currentTime.isAfter(scheduleTime.start) &&
|
||||
currentTime.isBefore(scheduleTime.end));
|
||||
} else {
|
||||
// Schedule times not available
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
//Make sure to pass correct millis values
|
||||
({TimeOfDay start, TimeOfDay end}) _getScheduleTime(
|
||||
int startMills, int endMills) {
|
||||
final sd = DateTime.fromMillisecondsSinceEpoch(startMills);
|
||||
final ed = DateTime.fromMillisecondsSinceEpoch(endMills);
|
||||
|
||||
final startTime = TimeOfDay(hour: sd.hour, minute: sd.minute);
|
||||
final endTime = TimeOfDay(hour: ed.hour, minute: ed.minute);
|
||||
|
||||
return (start: startTime, end: endTime);
|
||||
}
|
||||
|
||||
initializeSocket() {
|
||||
debugPrint('Socket address: ${WebUrls.socketUrl}');
|
||||
|
||||
_socketIO = io(
|
||||
WebUrls.socketUrl,
|
||||
OptionBuilder()
|
||||
.setTransports(['websocket']) // for Flutter or Dart VM
|
||||
.enableForceNewConnection()
|
||||
.enableAutoConnect()
|
||||
.setExtraHeaders({'foo': 'bar'}) // optional
|
||||
.build());
|
||||
|
||||
isSocketConnected.value = true;
|
||||
_socketIO.onConnect((_) {
|
||||
isSocketConnected.value = true;
|
||||
debugPrint('Socket Connected');
|
||||
});
|
||||
_socketIO.onDisconnect((_) {
|
||||
debugPrint('Socket Disconnected');
|
||||
// isSocketConnected.value = false;
|
||||
});
|
||||
_socketIO.onConnectError((e) {
|
||||
debugPrint('Socket Connection Error: ${e.toString()}');
|
||||
// isSocketConnected.value = false;
|
||||
});
|
||||
_socketIO.onConnectTimeout((e) {
|
||||
debugPrint('Socket timeout Error: ${e.toString()}');
|
||||
// isSocketConnected.value = false;
|
||||
});
|
||||
|
||||
//listenIdReceive
|
||||
listenId = (isGroup) ? args.groupData!.groupId : (myId + args.otherUserId);
|
||||
debugPrint('listen on: $listenId');
|
||||
_socketIO.on(listenId, (data) {
|
||||
debugPrint('listen on listenId: $data');
|
||||
_handleIncomingMessages(data);
|
||||
});
|
||||
}
|
||||
|
||||
_handleIncomingMessages(Map<String, dynamic> chatJson) {
|
||||
log("chat listen: $chatJson");
|
||||
// _total += 1;
|
||||
// _skip += 1;
|
||||
final chatModel = ChatModel.fromJson(chatJson);
|
||||
|
||||
if (chatJson.containsKey('createdAt')) {
|
||||
chatModel.date =
|
||||
DateTime.tryParse(chatJson["createdAt"])?.millisecondsSinceEpoch ?? 0;
|
||||
} else {
|
||||
chatModel.date = DateTime.now().millisecondsSinceEpoch;
|
||||
}
|
||||
|
||||
if (isGroup) {
|
||||
if (chatJson.containsKey('userId') && chatJson["userId"] is Map) {
|
||||
chatModel.from = UserData.fromJson(chatJson["userId"]);
|
||||
}
|
||||
_onFirstMessageSent();
|
||||
} else {
|
||||
chatModel.from = UserData(
|
||||
id: args.otherUserId,
|
||||
name: args.name,
|
||||
profilePictureUrl: args.profilePicPath);
|
||||
chatModel.to = UserData(id: chatJson['to']);
|
||||
}
|
||||
messages.insert(0, chatModel);
|
||||
args.onLastMessageUpdate?.call(chatModel);
|
||||
}
|
||||
|
||||
Future fetchSingleMessagesFromService() async {
|
||||
// _skip = 0;
|
||||
final response = await ChatService()
|
||||
.allSingleUsersChatMessagesServerAdmin(
|
||||
from: myId,
|
||||
to: args.otherUserId,
|
||||
)
|
||||
.showLoader();
|
||||
|
||||
if (response is List<ChatModel>) {
|
||||
if (response.isNotEmpty) {
|
||||
//Note: Converting this response to List of ChatModel objects
|
||||
// List<ChatModel> chats = [];
|
||||
// await Future.forEach(response.reversed, (e) {
|
||||
// chats.add(ChatModel(
|
||||
// id: e.id,
|
||||
// from: e.from,
|
||||
// to: e.to,
|
||||
// message: e.message,
|
||||
// date:
|
||||
// DateTime.tryParse(e.createdAt)?.millisecondsSinceEpoch ?? 0));
|
||||
// });
|
||||
messages.value = response.reversed.toList();
|
||||
}
|
||||
} else if (response is String) {
|
||||
FrequentFunctions.showToast(message: response);
|
||||
}
|
||||
}
|
||||
|
||||
void fetchGroupMessagesFromService(String idOfGroup) async {
|
||||
dynamic response = await ChatService().allGroupMessages(
|
||||
sortOrder: -1,
|
||||
offset: 0,
|
||||
limit: 100000,
|
||||
groupId: idOfGroup,
|
||||
isDeleted: false).showLoader();
|
||||
|
||||
if (response is List<AllGroupMessages>) {
|
||||
if (response.isNotEmpty) {
|
||||
//Note: Converting this response to List of ChatModel objects
|
||||
List<ChatModel> chats = [];
|
||||
await Future.forEach(response, (e) {
|
||||
chats.add(ChatModel(
|
||||
id: e.id,
|
||||
from: e.userId,
|
||||
message: e.message,
|
||||
messageType: e.messageType,
|
||||
filePath: e.filePath,
|
||||
date:
|
||||
DateTime.tryParse(e.createdAt)?.millisecondsSinceEpoch ?? 0));
|
||||
});
|
||||
messages.value = chats;
|
||||
}
|
||||
} else if (response is String) {
|
||||
FrequentFunctions.showToast(message: response);
|
||||
}
|
||||
}
|
||||
|
||||
void sendMessageButtonPressed() async {
|
||||
if (messageTEC.text.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
_sendMessage(
|
||||
message: messageTEC.text.trim(),
|
||||
);
|
||||
messageTEC.clear();
|
||||
}
|
||||
|
||||
//if sending first message, then updating chat list screen by calling it's listing api
|
||||
_onFirstMessageSent() {
|
||||
if (messages.length == 1) {
|
||||
try {
|
||||
final iController = Get.find<InboxScreenController>();
|
||||
iController.onFirstMessageSend.value = true;
|
||||
print("Got controller");
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_sendMessage({
|
||||
String? message,
|
||||
File? file,
|
||||
}) {
|
||||
if (isGroup) {
|
||||
_sendGroupMessage(
|
||||
message: message,
|
||||
file: file,
|
||||
);
|
||||
} else {
|
||||
_sendPrivateMessage(
|
||||
message: message,
|
||||
file: file,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///Note: no need to update ui for sent message here because
|
||||
///it's gonna handled by socket event
|
||||
_sendGroupMessage({
|
||||
String? message,
|
||||
File? file,
|
||||
}) async {
|
||||
await ChatService().addGroupMessageService(
|
||||
message: message ?? "",
|
||||
messageType: (file != null) ? MessageType.file : MessageType.message,
|
||||
file: file,
|
||||
userId: myId,
|
||||
groupId: args.groupData!.groupId,
|
||||
);
|
||||
}
|
||||
|
||||
///Note: handling messages list update also here
|
||||
_sendPrivateMessage({
|
||||
String? message,
|
||||
File? file,
|
||||
}) async {
|
||||
final id = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
final model = ChatModel(
|
||||
id: id.toString(),
|
||||
from: UserData(id: LocalStorageManager.userId),
|
||||
to: UserData(
|
||||
id: args.otherUserId,
|
||||
name: args.name,
|
||||
profilePictureUrl: args.profilePicPath,
|
||||
),
|
||||
message: message,
|
||||
messageType:
|
||||
(file != null) ? MessageType.file.name : MessageType.message.name,
|
||||
fileType: (file != null) ? ChatModel.fileTypeLocalPath : null,
|
||||
date: id,
|
||||
state: ChatModel.stateLoading,
|
||||
);
|
||||
|
||||
messages.insert(0, model);
|
||||
args.onLastMessageUpdate?.call(model);
|
||||
|
||||
dynamic response = await ChatService().addSingleMessage(
|
||||
message: message ?? "",
|
||||
messageType: (file != null) ? MessageType.file : MessageType.message,
|
||||
file: file,
|
||||
senderId: LocalStorageManager.userId,
|
||||
receiverId: args.otherUserId,
|
||||
);
|
||||
|
||||
final msg = messages.firstWhereOrNull((e) {
|
||||
return e.id == id.toString();
|
||||
});
|
||||
|
||||
if (msg != null) {
|
||||
final index = messages.indexOf(msg);
|
||||
|
||||
if (response is SingleChatModelClass) {
|
||||
//message sent successfully
|
||||
msg.id = response.id;
|
||||
msg.fileType = null;
|
||||
msg.state = ChatModel.stateSuccess;
|
||||
|
||||
messages
|
||||
..removeAt(index)
|
||||
..insert(index, msg);
|
||||
|
||||
_onFirstMessageSent();
|
||||
} else {
|
||||
msg.state = ChatModel.stateError;
|
||||
messages.removeAt(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pickAndSendFile() async {
|
||||
Get.focusScope?.unfocus();
|
||||
Get.bottomSheet(CupertinoActionSheet(
|
||||
actions: [
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
ImagePickerPopup.getImageFromSource(
|
||||
fromCamera: true,
|
||||
onFetchImage: (f) {
|
||||
_sendMessage(file: f);
|
||||
});
|
||||
},
|
||||
leading: const Icon(CupertinoIcons.camera),
|
||||
title: const Text("Camera"),
|
||||
trailing: Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
size: 18.r,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
FilePickerResult? result =
|
||||
await FilePicker.platform.pickFiles(type: FileType.media);
|
||||
|
||||
if (result != null && result.files.single.path != null) {
|
||||
if (result.files.single.path!.isImageFileName) {
|
||||
_sendMessage(file: File(result.files.single.path!));
|
||||
} else {
|
||||
FrequentFunctions.showToast(message: "File doesn't supported ");
|
||||
}
|
||||
}
|
||||
},
|
||||
leading: const Icon(CupertinoIcons.photo_on_rectangle),
|
||||
title: const Text("Gallery"),
|
||||
trailing: Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
size: 18.r,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
final FilePickerResult? result = await FilePicker.platform
|
||||
.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ["pdf", "doc", "docx", "xlsx", "xls"]);
|
||||
|
||||
if (result != null) {
|
||||
_sendMessage(file: File(result.files.single.path!));
|
||||
}
|
||||
},
|
||||
leading: const Icon(Icons.attach_file),
|
||||
title: const Text("File"),
|
||||
trailing: Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
size: 18.r,
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
// _sendImage(File file) async {
|
||||
// debugPrint("file: ${file.path}");
|
||||
// //
|
||||
// // ChatModel model = ChatModel(
|
||||
// // sentBy: UserData(id: myId),
|
||||
// // sentTo: otherUser,
|
||||
// // date: DateTime.now().toUtc().millisecondsSinceEpoch,
|
||||
// // files: [file.path],
|
||||
// // fileType: fileTypeLocalPath,
|
||||
// // state: ChatModel.stateLoading);
|
||||
// //
|
||||
// // final modelHash = model.hashCode.toString();
|
||||
// // model.localId = modelHash;
|
||||
// //
|
||||
// // debugPrint("message modelHash: $modelHash");
|
||||
// // _handleChat(model.toJson());
|
||||
// //
|
||||
// // // return;
|
||||
// // var res = await repository.sendChatAttachmentApi(
|
||||
// // req: ChatMessageRequest(
|
||||
// // sentBy: myId,
|
||||
// // sentTo: otherUser.id!,
|
||||
// // files: [file],
|
||||
// // fileType: "image"),
|
||||
// // );
|
||||
// //
|
||||
// // final i = messages.indexWhere((e) => e.localId == modelHash);
|
||||
// //
|
||||
// // if (res.success == true) {
|
||||
// // if (i != -1) {
|
||||
// // messages[i].state = ChatModel.stateSuccess;
|
||||
// // messages.refresh();
|
||||
// // }
|
||||
// // } else {
|
||||
// // if (i != -1) {
|
||||
// // messages[i].state = ChatModel.stateError;
|
||||
// // messages.refresh();
|
||||
// // }
|
||||
// // }
|
||||
// }
|
||||
|
||||
//Always call this method if _canChat is false
|
||||
void showCantMessageDialog() {
|
||||
final startMills = args.groupData!.scheduleTime.startTime;
|
||||
final endMills = args.groupData!.scheduleTime.endTime;
|
||||
|
||||
final scheduleTime = _getScheduleTime(startMills, endMills);
|
||||
final sd = DateTime(
|
||||
2024, 1, 1, scheduleTime.start.hour, scheduleTime.start.minute, 0);
|
||||
final ed =
|
||||
DateTime(2024, 1, 1, scheduleTime.end.hour, scheduleTime.end.minute, 0);
|
||||
|
||||
showDialog(
|
||||
context: screenKey.currentState!.context,
|
||||
builder: (BuildContext context) {
|
||||
return CustomMessageDialog(
|
||||
dialogButtonText: "Close",
|
||||
dialogMessageText:
|
||||
"It is currently outside the working hours. You can only send message during the working hours",
|
||||
dialogMessageTextBold:
|
||||
"Working hours: ${DateFormat("hh:mm aa").format(sd)} - ${DateFormat("hh:mm aa").format(ed)}",
|
||||
headingText: "You Can't message right now",
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// new
|
||||
void removeFocus() {
|
||||
FocusScope.of(screenKey.currentContext!).unfocus();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
try {
|
||||
_socketIO.clearListeners();
|
||||
_socketIO.disconnect();
|
||||
_socketIO.destroy();
|
||||
_socketIO.dispose();
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
messageTEC.dispose();
|
||||
messageFN.dispose();
|
||||
// scrollController.dispose();
|
||||
Get.delete<ChatScreenController>();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
89
lib/view/screens/chat/widgets/chat_screen_footer_widget.dart
Normal file
89
lib/view/screens/chat/widgets/chat_screen_footer_widget.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:ftc_mobile_app/ftc_mobile_app.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ftc_mobile_app/utilities/extensions/custom_extensions.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ChatScreenFooterWidget extends StatelessWidget {
|
||||
const ChatScreenFooterWidget({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.enabled,
|
||||
});
|
||||
|
||||
final ChatScreenController controller;
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: REdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: controller.pickAndSendFile,
|
||||
icon: SvgPicture.asset(
|
||||
AssetsManager.svgIcAdd,
|
||||
width: 32.r,
|
||||
height: 32.r,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
textAlign: TextAlign.left,
|
||||
controller: controller.messageTEC,
|
||||
keyboardType: TextInputType.multiline,
|
||||
focusNode: controller.messageFN,
|
||||
enabled: enabled,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type Message...',
|
||||
// border: InputBorder.none,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: 4.toRadius(),
|
||||
borderSide: BorderSide(
|
||||
width: 1,
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: 4.toRadius(),
|
||||
borderSide: BorderSide(
|
||||
width: 1,
|
||||
color: Get.theme.primaryColor,
|
||||
),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||
),
|
||||
),
|
||||
),
|
||||
5.horizontalSpace,
|
||||
SizedBox(
|
||||
width: 80.w,
|
||||
height: 40.r,
|
||||
child: IgnorePointer(
|
||||
ignoring: enabled.not,
|
||||
child: CustomAppButton(
|
||||
buttonText: "Send",
|
||||
buttonColor:
|
||||
enabled ? Get.theme.primaryColor : Colors.grey,
|
||||
borderColor:
|
||||
enabled ? Get.theme.primaryColor : Colors.grey,
|
||||
onTap: controller.sendMessageButtonPressed,
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
8.verticalSpace,
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
420
lib/view/screens/chat/widgets/message_bubble.dart
Normal file
420
lib/view/screens/chat/widgets/message_bubble.dart
Normal file
@@ -0,0 +1,420 @@
|
||||
import 'dart:io';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:ftc_mobile_app/utilities/extensions/custom_extensions.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:ftc_mobile_app/ftc_mobile_app.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../custom_widgets/my_circle_image.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
enum MessageType { sent, received }
|
||||
|
||||
enum MessageSeenStatus { delivered, seen }
|
||||
|
||||
enum MessageContentType { text, file, url }
|
||||
|
||||
enum MessageState {
|
||||
stateNone(0),
|
||||
stateError(-1),
|
||||
stateSending(1),
|
||||
stateSuccess(2);
|
||||
|
||||
final int intValue;
|
||||
|
||||
static MessageState stateFromIntValue(int value) {
|
||||
switch (value) {
|
||||
case -1:
|
||||
return MessageState.stateError;
|
||||
case 1:
|
||||
return MessageState.stateSending;
|
||||
case 2:
|
||||
return MessageState.stateSuccess;
|
||||
default:
|
||||
return MessageState.stateNone;
|
||||
}
|
||||
}
|
||||
|
||||
const MessageState(this.intValue);
|
||||
}
|
||||
|
||||
class MessageBubble extends StatelessWidget {
|
||||
final String senderName;
|
||||
final String content;
|
||||
final MessageState state;
|
||||
final MessageContentType contentType;
|
||||
final String profilePic;
|
||||
final MessageType type;
|
||||
final MessageSeenStatus status;
|
||||
final String messageTime;
|
||||
final Color? sentMessageColor;
|
||||
final Color? receivedMessageColor;
|
||||
final Color? sentMessageTextColor;
|
||||
final Color? receivedMessageTextColor;
|
||||
final bool showReportButton;
|
||||
|
||||
const MessageBubble({
|
||||
Key? key,
|
||||
required this.senderName,
|
||||
required this.content,
|
||||
required this.contentType,
|
||||
required this.state,
|
||||
required this.profilePic,
|
||||
required this.type,
|
||||
required this.status,
|
||||
required this.messageTime,
|
||||
this.sentMessageColor,
|
||||
this.receivedMessageColor,
|
||||
this.sentMessageTextColor,
|
||||
this.receivedMessageTextColor,
|
||||
this.showReportButton = true,
|
||||
}) : super(key: key);
|
||||
|
||||
Color get _backgroundColor => (type == MessageType.sent)
|
||||
? (sentMessageColor ?? Get.theme.primaryColor)
|
||||
: (receivedMessageColor ?? const Color(0xffC1C1C5));
|
||||
|
||||
Color get messageColor => (type == MessageType.sent)
|
||||
? (sentMessageTextColor ?? Colors.black)
|
||||
: (receivedMessageTextColor ?? Colors.white);
|
||||
|
||||
// double get _paddingLeft => (type == MessageType.sent) ? 0.15.sw : 0;
|
||||
|
||||
final radius = 16.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loader = Visibility(
|
||||
visible: (state == MessageState.stateSending),
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SizedBox.square(
|
||||
dimension: 18,
|
||||
child: CircularProgressIndicator(
|
||||
color: Get.theme.primaryColor,
|
||||
strokeWidth: 2,
|
||||
)),
|
||||
),
|
||||
);
|
||||
|
||||
final messageBox = SizedBox(
|
||||
width: 0.7.sw,
|
||||
child: Padding(
|
||||
padding: REdgeInsets.symmetric(vertical: 5),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
(type == MessageType.received)
|
||||
? MyCircleImage(
|
||||
imageSize: 20.r,
|
||||
url: WebUrls.baseUrl + profilePic,
|
||||
errorWidget: const DecoratedBox(
|
||||
decoration: BoxDecoration(color: Color(0xffC1C1C5)),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported_outlined,
|
||||
color: Colors.black,
|
||||
size: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
).paddingOnly(right: 8.r)
|
||||
: FrequentFunctions.noWidget,
|
||||
Expanded(
|
||||
child: (contentType == MessageContentType.text)
|
||||
? _textMessage()
|
||||
: (content.isImageFileName)
|
||||
? _imageWidget()
|
||||
: _documentFileWidget(),
|
||||
),
|
||||
],
|
||||
),
|
||||
4.verticalSpace,
|
||||
Row(
|
||||
children: [
|
||||
(type == MessageType.received)
|
||||
? SizedBox(width: 28.r)
|
||||
: FrequentFunctions.noWidget,
|
||||
(state == MessageState.stateSending)
|
||||
? loader
|
||||
: (state == MessageState.stateError)
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
color: Colors.red,
|
||||
size: 14.r,
|
||||
),
|
||||
4.horizontalSpace,
|
||||
Text(
|
||||
'Message not sent',
|
||||
style: const TextStyle().copyWith(
|
||||
color: Colors.red,
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
: FrequentFunctions.noWidget
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: (type == MessageType.sent)
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.start,
|
||||
children: [messageBox],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _textMessage() => Container(
|
||||
padding: REdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration:
|
||||
BoxDecoration(color: _backgroundColor, borderRadius: 10.toRadius()),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
(type == MessageType.received)
|
||||
? CustomTextWidget(
|
||||
text: senderName,
|
||||
fontColor: messageColor,
|
||||
textAlign: TextAlign.left,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
).paddingOnly(bottom: 10.r)
|
||||
: FrequentFunctions.noWidget,
|
||||
CustomTextWidget(
|
||||
text: content,
|
||||
fontColor: messageColor,
|
||||
textAlign: TextAlign.left,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
4.verticalSpace,
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: _messageTimeWidget(),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _imageWidget() {
|
||||
return InkWell(
|
||||
onTap: _openPreviewDialog,
|
||||
child: Container(
|
||||
width: 200.r,
|
||||
height: 250.r,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
padding: REdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: _backgroundColor,
|
||||
borderRadius: radius.toRadius()),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
//Sender Name
|
||||
(type == MessageType.received)
|
||||
? CustomTextWidget(
|
||||
text: senderName,
|
||||
fontColor: messageColor,
|
||||
textAlign: TextAlign.left,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
).paddingOnly(bottom: 10.r)
|
||||
: FrequentFunctions.noWidget,
|
||||
|
||||
//image
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
borderRadius: (radius - 4).toRadius(),
|
||||
child: (contentType == MessageContentType.file)
|
||||
? Image.file(File(content), fit: BoxFit.cover)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: (WebUrls.baseUrl + content),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
8.verticalSpace,
|
||||
|
||||
//Time
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: _messageTimeWidget(),
|
||||
).addPaddingHorizontal(12),
|
||||
8.verticalSpace,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _documentFileWidget() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: _backgroundColor,
|
||||
borderRadius: radius.toRadius(),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
padding: REdgeInsets.all(4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
//Sender Name
|
||||
(type == MessageType.received)
|
||||
? CustomTextWidget(
|
||||
text: senderName,
|
||||
fontColor: messageColor,
|
||||
textAlign: TextAlign.left,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
).paddingOnly(bottom: 10.r)
|
||||
: FrequentFunctions.noWidget,
|
||||
|
||||
//File
|
||||
InkWell(
|
||||
onTap: () {
|
||||
if (contentType == MessageContentType.url) {
|
||||
_launchUrl(WebUrls.baseUrl + content);
|
||||
} else {
|
||||
_launchUrl(content);
|
||||
}
|
||||
},
|
||||
child: Card(
|
||||
elevation: 0,
|
||||
surfaceTintColor: Colors.white,
|
||||
child: Padding(
|
||||
padding: REdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 24.r,
|
||||
backgroundColor:
|
||||
CustomAppColors.kSecondaryColor.withOpacity(0.1),
|
||||
child: const Icon(
|
||||
Icons.file_copy_outlined,
|
||||
color: CustomAppColors.kSecondaryColor,
|
||||
),
|
||||
),
|
||||
12.horizontalSpace,
|
||||
Expanded(
|
||||
child: CustomTextWidget(
|
||||
text: path.basename(content),
|
||||
fontColor: messageColor,
|
||||
textAlign: TextAlign.left,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
4.verticalSpace,
|
||||
|
||||
//Time
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: _messageTimeWidget(),
|
||||
).addPaddingHorizontal(12),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _messageTimeWidget() {
|
||||
return (messageTime.isNotEmpty)
|
||||
? Text(
|
||||
DateFormat("hh:mm aa")
|
||||
.format(DateTime.parse(messageTime).toLocal()),
|
||||
style: TextStyle(
|
||||
color: (type == MessageType.sent) ? Colors.grey : Colors.white,
|
||||
fontSize: 10.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
)
|
||||
: FrequentFunctions.noWidget;
|
||||
}
|
||||
|
||||
void _openPreviewDialog() {
|
||||
final img = (contentType == MessageContentType.file)
|
||||
? FileImage(File(content))
|
||||
: CachedNetworkImageProvider(WebUrls.baseUrl + content);
|
||||
Get.dialog(
|
||||
Material(
|
||||
type: MaterialType.transparency,
|
||||
child: SizedBox(
|
||||
width: double.maxFinite,
|
||||
height: double.maxFinite,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: PhotoViewGestureDetectorScope(
|
||||
axis: Axis.vertical,
|
||||
child: PhotoView(
|
||||
tightMode: true,
|
||||
backgroundDecoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
maxScale: PhotoViewComputedScale.covered * 1.1,
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
imageProvider: img as ImageProvider,
|
||||
heroAttributes:
|
||||
const PhotoViewHeroAttributes(tag: "someTag"),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 12,
|
||||
top: 12,
|
||||
child: InkWell(
|
||||
onTap: Get.back,
|
||||
child: Card(
|
||||
color: Colors.white,
|
||||
shape: 24.toRoundedRectRadius(),
|
||||
elevation: 4,
|
||||
child: RSizedBox.square(
|
||||
dimension: 40,
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: Colors.black,
|
||||
size: 24.r,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _launchUrl(String url) async {
|
||||
try {
|
||||
await launchUrl(Uri.parse(url));
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user